@onekeyfe/hd-core 1.2.0-alpha.17 → 1.2.0-alpha.19

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 (154) hide show
  1. package/__tests__/DeviceEventRegistration.test.ts +3 -0
  2. package/__tests__/device-features-state.test.ts +78 -0
  3. package/__tests__/device-identity.test.ts +30 -0
  4. package/__tests__/device-pool-state.test.ts +113 -0
  5. package/__tests__/device-settings.test.ts +108 -0
  6. package/__tests__/device-state-contract.test.ts +27 -0
  7. package/__tests__/device-state-events.test.ts +163 -0
  8. package/__tests__/device-state-mapper.test.ts +222 -0
  9. package/__tests__/device-state-projector.test.ts +94 -0
  10. package/__tests__/device-state-store.test.ts +115 -0
  11. package/__tests__/device-wallet-session-store.test.ts +46 -0
  12. package/__tests__/deviceFeaturesUtils.test.ts +13 -1
  13. package/__tests__/deviceSettings.test.ts +54 -0
  14. package/__tests__/get-device-state.test.ts +367 -0
  15. package/__tests__/kaspaSignTransaction.test.ts +591 -0
  16. package/__tests__/logBlockEvent.test.ts +13 -0
  17. package/__tests__/protocol-v2.test.ts +358 -413
  18. package/__tests__/public-device-state-api.test.ts +153 -0
  19. package/__tests__/refresh-device-state.test.ts +101 -0
  20. package/__tests__/request-context-logging.test.ts +16 -0
  21. package/__tests__/search-devices.test.ts +93 -0
  22. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  24. package/dist/api/GetDeviceState.d.ts +7 -0
  25. package/dist/api/GetDeviceState.d.ts.map +1 -0
  26. package/dist/api/GetFeatures.d.ts +1 -1
  27. package/dist/api/GetFeatures.d.ts.map +1 -1
  28. package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
  29. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  30. package/dist/api/SearchDevices.d.ts.map +1 -1
  31. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  32. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  33. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  34. package/dist/api/index.d.ts +1 -4
  35. package/dist/api/index.d.ts.map +1 -1
  36. package/dist/api/internal.d.ts +4 -0
  37. package/dist/api/internal.d.ts.map +1 -0
  38. package/dist/api/kaspa/KaspaSignTransaction.d.ts +5 -1
  39. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  40. package/dist/api/kaspa/helpers/TransferSerialize.d.ts.map +1 -1
  41. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +0 -1
  42. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  43. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  44. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -1
  45. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -1
  46. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -1
  47. package/dist/api/utils.d.ts +2 -1
  48. package/dist/api/utils.d.ts.map +1 -1
  49. package/dist/constants/index.d.ts +1 -1
  50. package/dist/constants/index.d.ts.map +1 -1
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/device/Device.d.ts +22 -3
  53. package/dist/device/Device.d.ts.map +1 -1
  54. package/dist/device/DeviceCommands.d.ts +4 -4
  55. package/dist/device/DeviceFeaturesState.d.ts +4 -0
  56. package/dist/device/DeviceFeaturesState.d.ts.map +1 -0
  57. package/dist/device/DevicePool.d.ts +1 -0
  58. package/dist/device/DevicePool.d.ts.map +1 -1
  59. package/dist/device/DeviceStateMapper.d.ts +13 -0
  60. package/dist/device/DeviceStateMapper.d.ts.map +1 -0
  61. package/dist/device/DeviceStateProjector.d.ts +8 -0
  62. package/dist/device/DeviceStateProjector.d.ts.map +1 -0
  63. package/dist/device/DeviceStateStore.d.ts +23 -0
  64. package/dist/device/DeviceStateStore.d.ts.map +1 -0
  65. package/dist/device/deviceIdentity.d.ts +5 -0
  66. package/dist/device/deviceIdentity.d.ts.map +1 -0
  67. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  68. package/dist/deviceProfile/index.d.ts +0 -1
  69. package/dist/deviceProfile/index.d.ts.map +1 -1
  70. package/dist/events/device.d.ts +7 -2
  71. package/dist/events/device.d.ts.map +1 -1
  72. package/dist/index.d.ts +722 -648
  73. package/dist/index.js +1863 -1033
  74. package/dist/inject.d.ts.map +1 -1
  75. package/dist/protocols/protocol-v2/features.d.ts +1 -0
  76. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  77. package/dist/protocols/protocol-v2/index.d.ts +1 -1
  78. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  79. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  80. package/dist/types/api/deviceSettings.d.ts +8 -0
  81. package/dist/types/api/deviceSettings.d.ts.map +1 -1
  82. package/dist/types/api/getDeviceState.d.ts +12 -0
  83. package/dist/types/api/getDeviceState.d.ts.map +1 -0
  84. package/dist/types/api/index.d.ts +4 -7
  85. package/dist/types/api/index.d.ts.map +1 -1
  86. package/dist/types/api/kaspaSignTransaction.d.ts +27 -2
  87. package/dist/types/api/kaspaSignTransaction.d.ts.map +1 -1
  88. package/dist/types/api/protocolV2.d.ts +1 -6
  89. package/dist/types/api/protocolV2.d.ts.map +1 -1
  90. package/dist/types/device.d.ts +121 -1
  91. package/dist/types/device.d.ts.map +1 -1
  92. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  93. package/dist/utils/deviceSettings.d.ts.map +1 -1
  94. package/dist/utils/patch.d.ts +1 -1
  95. package/dist/utils/patch.d.ts.map +1 -1
  96. package/dist/utils/tracing.d.ts.map +1 -1
  97. package/package.json +4 -4
  98. package/src/api/ClearSessionCache.ts +4 -0
  99. package/src/api/FirmwareUpdateV4.ts +15 -7
  100. package/src/api/GetDeviceState.ts +55 -0
  101. package/src/api/GetFeatures.ts +8 -3
  102. package/src/api/GetOnekeyFeatures.ts +3 -72
  103. package/src/api/GetPassphraseState.ts +3 -1
  104. package/src/api/SearchDevices.ts +25 -4
  105. package/src/api/device/DeviceRebootToBoardloader.ts +1 -0
  106. package/src/api/device/DeviceRebootToBootloader.ts +1 -0
  107. package/src/api/device/DeviceSettings.ts +28 -0
  108. package/src/api/index.ts +1 -4
  109. package/src/api/internal.ts +9 -0
  110. package/src/api/kaspa/KaspaSignTransaction.ts +341 -22
  111. package/src/api/kaspa/helpers/TransferSerialize.ts +14 -0
  112. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -5
  113. package/src/api/protocol-v2/DeviceReboot.ts +3 -1
  114. package/src/api/protocol-v2/DeviceSettingsGet.ts +3 -1
  115. package/src/api/protocol-v2/DeviceSettingsSet.ts +2 -0
  116. package/src/api/protocol-v2/DeviceStatusGet.ts +1 -0
  117. package/src/api/utils.ts +9 -3
  118. package/src/constants/index.ts +1 -4
  119. package/src/core/index.ts +7 -1
  120. package/src/data/messages/messages-protocol-v2.json +22 -13
  121. package/src/data/messages/messages.json +286 -1
  122. package/src/device/Device.ts +256 -78
  123. package/src/device/DeviceFeaturesState.ts +34 -0
  124. package/src/device/DevicePool.ts +35 -9
  125. package/src/device/DeviceStateMapper.ts +364 -0
  126. package/src/device/DeviceStateProjector.ts +92 -0
  127. package/src/device/DeviceStateStore.ts +242 -0
  128. package/src/device/deviceIdentity.ts +18 -0
  129. package/src/deviceProfile/buildDeviceFeatures.ts +166 -109
  130. package/src/deviceProfile/index.ts +0 -1
  131. package/src/events/device.ts +13 -1
  132. package/src/events/logBlockEvent.ts +1 -1
  133. package/src/inject.ts +4 -5
  134. package/src/protocols/protocol-v2/features.ts +18 -9
  135. package/src/protocols/protocol-v2/index.ts +1 -0
  136. package/src/protocols/protocol-v2/walletSession.ts +5 -7
  137. package/src/types/api/deviceSettings.ts +8 -0
  138. package/src/types/api/getDeviceState.ts +19 -0
  139. package/src/types/api/index.ts +3 -20
  140. package/src/types/api/kaspaSignTransaction.ts +40 -2
  141. package/src/types/api/protocolV2.ts +0 -24
  142. package/src/types/device.ts +148 -1
  143. package/src/utils/deviceFeaturesUtils.ts +5 -1
  144. package/src/utils/deviceSettings.ts +41 -0
  145. package/src/utils/tracing.ts +3 -1
  146. package/dist/api/GetDeviceInfo.d.ts +0 -9
  147. package/dist/api/GetDeviceInfo.d.ts.map +0 -1
  148. package/dist/deviceProfile/buildDeviceProfile.d.ts +0 -22
  149. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +0 -1
  150. package/dist/types/api/getDeviceInfo.d.ts +0 -88
  151. package/dist/types/api/getDeviceInfo.d.ts.map +0 -1
  152. package/src/api/GetDeviceInfo.ts +0 -152
  153. package/src/deviceProfile/buildDeviceProfile.ts +0 -370
  154. package/src/types/api/getDeviceInfo.ts +0 -106
@@ -6,6 +6,7 @@ const handlers = {
6
6
  button: jest.fn(),
7
7
  passphrase: jest.fn(),
8
8
  passphraseOnDevice: jest.fn(),
9
+ attachPinOnDevice: jest.fn(),
9
10
  };
10
11
 
11
12
  const createDevice = (protocolV2: boolean) => ({
@@ -23,6 +24,7 @@ describe('hardware UI event registration', () => {
23
24
  DEVICE.BUTTON,
24
25
  DEVICE.PASSPHRASE,
25
26
  DEVICE.PASSPHRASE_ON_DEVICE,
27
+ DEVICE.ATTACH_PIN_ON_DEVICE,
26
28
  ]);
27
29
  });
28
30
 
@@ -35,6 +37,7 @@ describe('hardware UI event registration', () => {
35
37
  DEVICE.BUTTON,
36
38
  DEVICE.PASSPHRASE,
37
39
  DEVICE.PASSPHRASE_ON_DEVICE,
40
+ DEVICE.ATTACH_PIN_ON_DEVICE,
38
41
  ]);
39
42
  });
40
43
  });
@@ -0,0 +1,78 @@
1
+ import { mergeDeviceFeaturesPatch } from '../src/device/DeviceFeaturesState';
2
+
3
+ import type { Features } from '../src/types';
4
+
5
+ const baseFeatures = {
6
+ protocol: 'V2',
7
+ deviceType: 'pro2',
8
+ firmwareType: 'universal',
9
+ model: 'pro2',
10
+ vendor: 'onekey.so',
11
+ deviceId: null,
12
+ serialNo: 'P20001',
13
+ label: 'Old Label',
14
+ bleName: 'Pro2 0001',
15
+ capabilities: [],
16
+ mode: 'normal',
17
+ initialized: true,
18
+ bootloaderMode: false,
19
+ unlocked: true,
20
+ firmwarePresent: true,
21
+ passphraseProtection: false,
22
+ pinProtection: true,
23
+ backupRequired: false,
24
+ noBackup: false,
25
+ unfinishedBackup: false,
26
+ recoveryMode: false,
27
+ language: 'en-US',
28
+ bleEnabled: true,
29
+ sdCardPresent: null,
30
+ sdProtection: null,
31
+ wipeCodeProtection: null,
32
+ passphraseAlwaysOnDevice: null,
33
+ safetyChecks: null,
34
+ autoLockDelayMs: 60_000,
35
+ displayRotation: null,
36
+ experimentalFeatures: false,
37
+ firmwareVersion: '1.0.0',
38
+ bootloaderVersion: '1.0.0',
39
+ boardVersion: '1.0.0',
40
+ bleVersion: '1.0.0',
41
+ sessionId: null,
42
+ } as Features;
43
+
44
+ describe('mergeDeviceFeaturesPatch', () => {
45
+ it('ignores undefined fields instead of clearing cached values', () => {
46
+ const next = mergeDeviceFeaturesPatch(baseFeatures, {
47
+ label: undefined,
48
+ language: 'ja-JP',
49
+ });
50
+
51
+ expect(next).toMatchObject({
52
+ label: 'Old Label',
53
+ language: 'ja-JP',
54
+ });
55
+ });
56
+
57
+ it('allows null to explicitly clear a cached value', () => {
58
+ const next = mergeDeviceFeaturesPatch(baseFeatures, { label: null });
59
+
60
+ expect(next.label).toBeNull();
61
+ });
62
+
63
+ it('returns the same object when no value changes', () => {
64
+ const next = mergeDeviceFeaturesPatch(baseFeatures, {
65
+ label: 'Old Label',
66
+ language: undefined,
67
+ });
68
+
69
+ expect(next).toBe(baseFeatures);
70
+ });
71
+
72
+ it('returns a new object when a defined value changes', () => {
73
+ const next = mergeDeviceFeaturesPatch(baseFeatures, { label: 'New Label' });
74
+
75
+ expect(next).not.toBe(baseFeatures);
76
+ expect(next.label).toBe('New Label');
77
+ });
78
+ });
@@ -0,0 +1,30 @@
1
+ import { checkLiveDeviceId } from '../src/device/deviceIdentity';
2
+
3
+ describe('live device identity', () => {
4
+ test('refreshes Protocol V2 status before comparing the expected device id', async () => {
5
+ let currentDeviceId = 'OLD_DEVICE_ID';
6
+ const device = {
7
+ isProtocolV2: () => true,
8
+ getDeviceState: jest.fn().mockImplementation(async () => {
9
+ currentDeviceId = 'NEW_DEVICE_ID';
10
+ return { identity: { deviceId: currentDeviceId } };
11
+ }),
12
+ checkDeviceId: jest.fn((expected: string) => currentDeviceId === expected),
13
+ };
14
+
15
+ await expect(checkLiveDeviceId(device as never, 'OLD_DEVICE_ID')).resolves.toBe(false);
16
+ expect(device.getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status'] });
17
+ expect(device.checkDeviceId).toHaveBeenCalledWith('OLD_DEVICE_ID');
18
+ });
19
+
20
+ test('keeps Protocol V1 identity checks on the Initialize/GetFeatures state', async () => {
21
+ const device = {
22
+ isProtocolV2: () => false,
23
+ getDeviceState: jest.fn(),
24
+ checkDeviceId: jest.fn(() => true),
25
+ };
26
+
27
+ await expect(checkLiveDeviceId(device as never, 'V1_DEVICE_ID')).resolves.toBe(true);
28
+ expect(device.getDeviceState).not.toHaveBeenCalled();
29
+ });
30
+ });
@@ -0,0 +1,113 @@
1
+ import { DevicePool } from '../src/device/DevicePool';
2
+ import { Device } from '../src/device/Device';
3
+ import { DEVICE } from '../src/events';
4
+
5
+ jest.mock('../src/data/config', () => ({
6
+ getSDKVersion: jest.fn(() => '1.0.0'),
7
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
8
+ }));
9
+
10
+ describe('DevicePool state lifecycle', () => {
11
+ beforeEach(() => {
12
+ DevicePool.resetState();
13
+ });
14
+
15
+ afterEach(() => {
16
+ DevicePool.resetState();
17
+ });
18
+
19
+ test('refreshes runtime state for a cached Protocol V2 discovery result', async () => {
20
+ const descriptor = { path: 'pro2-path', protocolType: 'V2' } as any;
21
+ const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'normal' } });
22
+ const device = {
23
+ originalDescriptor: descriptor,
24
+ updateDescriptor: jest.fn(),
25
+ isProtocolV2: () => true,
26
+ getDeviceState,
27
+ run: jest.fn(async (callback: () => Promise<void>) => callback()),
28
+ } as any;
29
+ DevicePool.devicesCache = { 'pro2-id': device };
30
+
31
+ await DevicePool.getDevices([descriptor], 'pro2-id', {
32
+ connectProtocol: 'V2',
33
+ refreshRuntimeState: true,
34
+ });
35
+
36
+ expect(device.run).toHaveBeenCalledTimes(1);
37
+ expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status'] });
38
+ });
39
+
40
+ test('uses the disconnected descriptor and invalidates its cached state', () => {
41
+ const descriptor = { path: 'pro2-path', protocolType: 'V2' } as any;
42
+ const device = {
43
+ originalDescriptor: descriptor,
44
+ markTransportDisconnected: jest.fn(),
45
+ } as any;
46
+ DevicePool.devicesCache = { 'pro2-id': device };
47
+ DevicePool.disconnectPool = [descriptor];
48
+ const onDisconnect = jest.fn();
49
+ DevicePool.emitter.on(DEVICE.DISCONNECT, onDisconnect);
50
+
51
+ (DevicePool as any)._sendDisconnectMessage();
52
+
53
+ expect(device.markTransportDisconnected).toHaveBeenCalledTimes(1);
54
+ expect(onDisconnect).toHaveBeenCalledWith(device);
55
+ expect(DevicePool.disconnectPool).toEqual([]);
56
+ });
57
+
58
+ test('releases a newly created device when initialization fails', async () => {
59
+ const descriptor = { path: 'pro2-path', protocolType: 'V2' } as any;
60
+ const release = jest.fn().mockResolvedValue(undefined);
61
+ const device = {
62
+ connect: jest.fn().mockResolvedValue(true),
63
+ initialize: jest.fn().mockRejectedValue(new Error('initialize failed')),
64
+ release,
65
+ } as any;
66
+ const fromDescriptor = jest.spyOn(Device, 'fromDescriptor').mockReturnValue(device);
67
+
68
+ try {
69
+ await expect((DevicePool as any)._createDevice(descriptor)).rejects.toThrow(
70
+ 'initialize failed'
71
+ );
72
+ } finally {
73
+ fromDescriptor.mockRestore();
74
+ }
75
+
76
+ expect(release).toHaveBeenCalledTimes(1);
77
+ });
78
+
79
+ test('releases the cached device before surfacing a runtime refresh error', async () => {
80
+ const refreshError = new Error('status failed');
81
+ let callbackRejected = false;
82
+ const device = {
83
+ isProtocolV2: () => true,
84
+ getDeviceState: jest.fn().mockRejectedValue(refreshError),
85
+ run: jest.fn(async (callback: () => Promise<void>) => {
86
+ try {
87
+ await callback();
88
+ } catch {
89
+ callbackRejected = true;
90
+ }
91
+ }),
92
+ } as any;
93
+
94
+ await expect(
95
+ (DevicePool as any)._refreshRuntimeState(device, { refreshRuntimeState: true })
96
+ ).rejects.toBe(refreshError);
97
+ expect(callbackRejected).toBe(false);
98
+ });
99
+
100
+ test('releases an acquired device when reinitialization fails', async () => {
101
+ const device = Device.fromDescriptor({ path: 'pro2-path', protocolType: 'V2' } as any);
102
+ (device as any).commands = { disposed: true };
103
+ jest.spyOn(device, 'acquire').mockResolvedValue(true);
104
+ jest.spyOn(device, 'initialize').mockRejectedValue(new Error('reinitialize failed'));
105
+ const release = jest.spyOn(device, 'release').mockResolvedValue(undefined);
106
+
107
+ await expect(device.run(jest.fn().mockResolvedValue(undefined))).rejects.toThrow(
108
+ 'reinitialize failed'
109
+ );
110
+
111
+ expect(release).toHaveBeenCalledTimes(1);
112
+ });
113
+ });
@@ -0,0 +1,108 @@
1
+ import DeviceSettings from '../src/api/device/DeviceSettings';
2
+
3
+ import type { Features } from '../src/types';
4
+
5
+ jest.mock('../src/data/config', () => ({
6
+ DEFAULT_DOMAIN: 'https://example.com/',
7
+ getSDKVersion: () => '0.0.0-test',
8
+ }));
9
+
10
+ const features = {
11
+ protocol: 'V2',
12
+ label: 'Old Label',
13
+ language: 'en-US',
14
+ } as Features;
15
+
16
+ function createDevice({ protocol }: { protocol: 'V1' | 'V2' }) {
17
+ const typedCall = jest.fn().mockResolvedValue({ message: { message: 'Success' } });
18
+ const updateState = jest.fn();
19
+ return {
20
+ device: {
21
+ features,
22
+ isProtocolV2: () => protocol === 'V2',
23
+ commands: { typedCall },
24
+ updateState,
25
+ },
26
+ typedCall,
27
+ updateState,
28
+ };
29
+ }
30
+
31
+ describe('DeviceSettings protocol routing', () => {
32
+ it('uses ApplySettings and commits the confirmed patch for Protocol V1', async () => {
33
+ const { device, typedCall, updateState } = createDevice({ protocol: 'V1' });
34
+ const method = new DeviceSettings({
35
+ id: 1,
36
+ payload: {
37
+ method: 'deviceSettings',
38
+ label: 'Shared Label',
39
+ language: 'ja-JP',
40
+ },
41
+ });
42
+ method.init();
43
+ (method as any).device = device;
44
+
45
+ await expect(method.run()).resolves.toEqual({ message: 'Success' });
46
+ expect(typedCall).toHaveBeenCalledWith('ApplySettings', 'Success', {
47
+ label: 'Shared Label',
48
+ language: 'ja-JP',
49
+ use_passphrase: undefined,
50
+ homescreen: undefined,
51
+ _passphrase_source: undefined,
52
+ auto_lock_delay_ms: undefined,
53
+ display_rotation: undefined,
54
+ passphrase_always_on_device: undefined,
55
+ safety_checks: undefined,
56
+ experimental_features: undefined,
57
+ auto_shutdown_delay_ms: undefined,
58
+ haptic_feedback: undefined,
59
+ });
60
+ expect(updateState).toHaveBeenCalledWith(
61
+ {
62
+ identity: { label: 'Shared Label' },
63
+ settings: { language: 'ja-JP' },
64
+ },
65
+ 'apply-settings'
66
+ );
67
+ });
68
+
69
+ it('uses DeviceSettingsSet and commits the confirmed patch for Protocol V2', async () => {
70
+ const { device, typedCall, updateState } = createDevice({ protocol: 'V2' });
71
+ const method = new DeviceSettings({
72
+ id: 2,
73
+ payload: {
74
+ method: 'deviceSettings',
75
+ label: 'Shared Label',
76
+ language: 'ja-JP',
77
+ autoLockDelayMs: 60_000,
78
+ hapticFeedback: true,
79
+ },
80
+ });
81
+ method.init();
82
+ (method as any).device = device;
83
+
84
+ await expect(method.run()).resolves.toEqual({ message: 'Success' });
85
+ expect(typedCall).toHaveBeenCalledWith('DeviceSettingsSet', 'Success', {
86
+ settings: {
87
+ label: 'Shared Label',
88
+ language: 'ja-JP',
89
+ autolock_delay_ms: 60_000,
90
+ haptic_feedback: true,
91
+ },
92
+ });
93
+ expect(updateState).toHaveBeenCalledWith(
94
+ {
95
+ identity: { label: 'Shared Label' },
96
+ settings: {
97
+ language: 'ja-JP',
98
+ autoLockDelayMs: 60_000,
99
+ hapticFeedback: true,
100
+ },
101
+ },
102
+ 'apply-settings'
103
+ );
104
+ expect(typedCall.mock.calls.map(call => call[0])).not.toEqual(
105
+ expect.arrayContaining(['DeviceInfoGet', 'DeviceStatusGet', 'DeviceSettingsGet'])
106
+ );
107
+ });
108
+ });
@@ -0,0 +1,27 @@
1
+ import { DEVICE } from '../src/events';
2
+ import type { DeviceState, DeviceStateEvent } from '../src/types';
3
+
4
+ describe('DeviceState contract', () => {
5
+ test('exports the unified device state event contract', () => {
6
+ expect(DEVICE.STATE).toBe('state');
7
+
8
+ const state = {} as DeviceState;
9
+ const event: DeviceStateEvent = {
10
+ connectId: 'usb-1',
11
+ state,
12
+ revision: 1,
13
+ source: 'initialize',
14
+ changedKeys: ['identity.label'],
15
+ };
16
+
17
+ expect(event.state).toBe(state);
18
+ });
19
+ });
20
+
21
+ // raw 仅供 SDK 内部兼容投影使用,不属于公共 DeviceState。
22
+ // @ts-expect-error raw 不应成为外部接入者需要理解的字段
23
+ type RemovedPublicRaw = DeviceState['raw'];
24
+
25
+ // session 属于 SDK 运行时内部缓存,不应通过公共状态或事件暴露。
26
+ // @ts-expect-error session 不属于公共 DeviceState
27
+ type RemovedPublicSession = DeviceState['session'];
@@ -0,0 +1,163 @@
1
+ import { Device } from '../src/device/Device';
2
+ import { DEVICE } from '../src/events';
3
+ import { EOneKeyDeviceMode } from '../src/types';
4
+
5
+ jest.mock('../src/data/config', () => ({
6
+ getSDKVersion: jest.fn(() => '1.0.0'),
7
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
8
+ }));
9
+
10
+ describe('Device state events', () => {
11
+ test('emits only the canonical state event for Protocol V2', () => {
12
+ const device = Device.fromDescriptor({
13
+ id: 'pro2',
14
+ path: 'pro2',
15
+ protocolType: 'V2',
16
+ } as never);
17
+ const onState = jest.fn();
18
+ const onFeatures = jest.fn();
19
+ device.on(DEVICE.STATE, onState);
20
+ device.on(DEVICE.FEATURES, onFeatures);
21
+
22
+ const state = device.updateState(
23
+ {
24
+ protocol: 'V2',
25
+ identity: { label: 'Renamed', bleName: 'Pro2 1234' },
26
+ session: { sessionId: 'private-session', passphraseState: 'private-state' },
27
+ raw: {
28
+ protocolV2DeviceInfo: { protocol_version: 2 },
29
+ },
30
+ },
31
+ 'apply-settings'
32
+ );
33
+
34
+ expect(state?.identity.displayName).toBe('Renamed');
35
+ expect(onState).toHaveBeenCalledWith(
36
+ device,
37
+ expect.objectContaining({
38
+ state: expect.objectContaining({ revision: 1 }),
39
+ revision: 1,
40
+ source: 'apply-settings',
41
+ })
42
+ );
43
+ expect(onFeatures).not.toHaveBeenCalled();
44
+ expect(onState.mock.calls[0][1].state).not.toHaveProperty('raw');
45
+ expect(onState.mock.calls[0][1].state).not.toHaveProperty('session');
46
+ expect(device.state?.raw?.protocolV2DeviceInfo).toEqual({ protocol_version: 2 });
47
+ });
48
+
49
+ test.each([
50
+ ['normal', null, EOneKeyDeviceMode.normal],
51
+ ['notInitialized', false, EOneKeyDeviceMode.notInitialized],
52
+ ['backupMode', true, EOneKeyDeviceMode.backupMode],
53
+ ['bootloader', null, EOneKeyDeviceMode.bootloader],
54
+ ['romloader', null, EOneKeyDeviceMode.bootloader],
55
+ ] as const)(
56
+ 'uses canonical %s mode even when initialized is %s',
57
+ (mode, initialized, expected) => {
58
+ const device = Device.fromDescriptor({
59
+ id: 'mode',
60
+ path: 'mode',
61
+ protocolType: 'V2',
62
+ } as never);
63
+ device.updateState({ protocol: 'V2', status: { mode, initialized } }, 'initialize');
64
+
65
+ expect(device.getMode()).toBe(expected);
66
+ }
67
+ );
68
+
69
+ test('commits lock success to canonical state and emits one state event', async () => {
70
+ const device = Device.fromDescriptor({ id: 'pro2', path: 'pro2', protocolType: 'V2' } as never);
71
+ (device as any).commands = {
72
+ typedCall: jest.fn().mockResolvedValue({ message: { message: 'locked' } }),
73
+ };
74
+ device.updateState(
75
+ { protocol: 'V2', status: { mode: 'normal', initialized: true, unlocked: true } },
76
+ 'initialize'
77
+ );
78
+ const onState = jest.fn();
79
+ device.on(DEVICE.STATE, onState);
80
+
81
+ await device.lockDevice();
82
+
83
+ expect(device.state?.status.unlocked).toBe(false);
84
+ expect(onState).toHaveBeenCalledWith(
85
+ device,
86
+ expect.objectContaining({ source: 'lock', changedKeys: ['status.unlocked'] })
87
+ );
88
+ });
89
+
90
+ test('continues emitting projected Features events for Protocol V1', () => {
91
+ const device = Device.fromDescriptor({ id: 'legacy', path: 'legacy' } as never);
92
+ const onFeatures = jest.fn();
93
+ device.on(DEVICE.FEATURES, onFeatures);
94
+
95
+ device.updateState(
96
+ {
97
+ protocol: 'V1',
98
+ identity: { label: 'Legacy' },
99
+ },
100
+ 'apply-settings'
101
+ );
102
+
103
+ expect(onFeatures).toHaveBeenCalledWith(device, expect.objectContaining({ label: 'Legacy' }));
104
+ });
105
+
106
+ test('keeps Features assignment as an import-only compatibility projection', () => {
107
+ const device = Device.fromDescriptor({ id: 'legacy', path: 'legacy' } as never);
108
+
109
+ (device as any).features = {
110
+ protocol: 'V1',
111
+ deviceType: 'pro',
112
+ firmwareType: 'universal',
113
+ model: 'pro',
114
+ vendor: 'onekey.so',
115
+ deviceId: 'device-1',
116
+ serialNo: 'SERIAL-1',
117
+ label: 'Imported',
118
+ bleName: null,
119
+ capabilities: [],
120
+ mode: 'normal',
121
+ initialized: true,
122
+ bootloaderMode: false,
123
+ unlocked: true,
124
+ firmwarePresent: true,
125
+ passphraseProtection: false,
126
+ pinProtection: true,
127
+ backupRequired: false,
128
+ noBackup: false,
129
+ unfinishedBackup: false,
130
+ recoveryMode: false,
131
+ language: 'en-US',
132
+ bleEnabled: true,
133
+ sdCardPresent: false,
134
+ sdProtection: false,
135
+ wipeCodeProtection: false,
136
+ passphraseAlwaysOnDevice: false,
137
+ safetyChecks: null,
138
+ autoLockDelayMs: 10_000,
139
+ autoShutdownDelayMs: 20_000,
140
+ displayRotation: 0,
141
+ experimentalFeatures: false,
142
+ wallpaperPath: null,
143
+ brightness: 50,
144
+ animationEnabled: true,
145
+ tapToWake: true,
146
+ hapticFeedback: true,
147
+ deviceNameDisplayEnabled: true,
148
+ airgapMode: false,
149
+ fidoEnabled: true,
150
+ usbLockEnabled: false,
151
+ randomKeypad: false,
152
+ firmwareVersion: '4.0.0',
153
+ bootloaderVersion: '1.0.0',
154
+ boardVersion: '1.0.0',
155
+ bleVersion: '1.0.0',
156
+ sessionId: null,
157
+ };
158
+
159
+ expect(device.state?.identity.label).toBe('Imported');
160
+ expect(device.features).not.toBe((device as any).state);
161
+ expect(device.features?.label).toBe('Imported');
162
+ });
163
+ });