@onekeyfe/hd-core 1.2.0-alpha.18 → 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 (141) hide show
  1. package/__tests__/device-features-state.test.ts +78 -0
  2. package/__tests__/device-identity.test.ts +30 -0
  3. package/__tests__/device-pool-state.test.ts +113 -0
  4. package/__tests__/device-settings.test.ts +108 -0
  5. package/__tests__/device-state-contract.test.ts +27 -0
  6. package/__tests__/device-state-events.test.ts +163 -0
  7. package/__tests__/device-state-mapper.test.ts +222 -0
  8. package/__tests__/device-state-projector.test.ts +94 -0
  9. package/__tests__/device-state-store.test.ts +115 -0
  10. package/__tests__/device-wallet-session-store.test.ts +46 -0
  11. package/__tests__/deviceFeaturesUtils.test.ts +13 -1
  12. package/__tests__/deviceSettings.test.ts +54 -0
  13. package/__tests__/get-device-state.test.ts +367 -0
  14. package/__tests__/logBlockEvent.test.ts +13 -0
  15. package/__tests__/protocol-v2.test.ts +358 -413
  16. package/__tests__/public-device-state-api.test.ts +153 -0
  17. package/__tests__/refresh-device-state.test.ts +101 -0
  18. package/__tests__/request-context-logging.test.ts +16 -0
  19. package/__tests__/search-devices.test.ts +93 -0
  20. package/dist/api/ClearSessionCache.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/GetDeviceState.d.ts +7 -0
  23. package/dist/api/GetDeviceState.d.ts.map +1 -0
  24. package/dist/api/GetFeatures.d.ts +1 -1
  25. package/dist/api/GetFeatures.d.ts.map +1 -1
  26. package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
  27. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  28. package/dist/api/SearchDevices.d.ts.map +1 -1
  29. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  30. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  31. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  32. package/dist/api/index.d.ts +1 -4
  33. package/dist/api/index.d.ts.map +1 -1
  34. package/dist/api/internal.d.ts +4 -0
  35. package/dist/api/internal.d.ts.map +1 -0
  36. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +0 -1
  37. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  38. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  39. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -1
  40. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -1
  41. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -1
  42. package/dist/api/utils.d.ts +2 -1
  43. package/dist/api/utils.d.ts.map +1 -1
  44. package/dist/constants/index.d.ts +1 -1
  45. package/dist/constants/index.d.ts.map +1 -1
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/device/Device.d.ts +22 -3
  48. package/dist/device/Device.d.ts.map +1 -1
  49. package/dist/device/DeviceFeaturesState.d.ts +4 -0
  50. package/dist/device/DeviceFeaturesState.d.ts.map +1 -0
  51. package/dist/device/DevicePool.d.ts +1 -0
  52. package/dist/device/DevicePool.d.ts.map +1 -1
  53. package/dist/device/DeviceStateMapper.d.ts +13 -0
  54. package/dist/device/DeviceStateMapper.d.ts.map +1 -0
  55. package/dist/device/DeviceStateProjector.d.ts +8 -0
  56. package/dist/device/DeviceStateProjector.d.ts.map +1 -0
  57. package/dist/device/DeviceStateStore.d.ts +23 -0
  58. package/dist/device/DeviceStateStore.d.ts.map +1 -0
  59. package/dist/device/deviceIdentity.d.ts +5 -0
  60. package/dist/device/deviceIdentity.d.ts.map +1 -0
  61. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  62. package/dist/deviceProfile/index.d.ts +0 -1
  63. package/dist/deviceProfile/index.d.ts.map +1 -1
  64. package/dist/events/device.d.ts +7 -2
  65. package/dist/events/device.d.ts.map +1 -1
  66. package/dist/index.d.ts +688 -639
  67. package/dist/index.js +1274 -942
  68. package/dist/inject.d.ts.map +1 -1
  69. package/dist/protocols/protocol-v2/features.d.ts +1 -0
  70. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  71. package/dist/protocols/protocol-v2/index.d.ts +1 -1
  72. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  74. package/dist/types/api/deviceSettings.d.ts +8 -0
  75. package/dist/types/api/deviceSettings.d.ts.map +1 -1
  76. package/dist/types/api/getDeviceState.d.ts +12 -0
  77. package/dist/types/api/getDeviceState.d.ts.map +1 -0
  78. package/dist/types/api/index.d.ts +4 -7
  79. package/dist/types/api/index.d.ts.map +1 -1
  80. package/dist/types/api/protocolV2.d.ts +1 -6
  81. package/dist/types/api/protocolV2.d.ts.map +1 -1
  82. package/dist/types/device.d.ts +121 -1
  83. package/dist/types/device.d.ts.map +1 -1
  84. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  85. package/dist/utils/deviceSettings.d.ts.map +1 -1
  86. package/dist/utils/patch.d.ts +1 -1
  87. package/dist/utils/tracing.d.ts.map +1 -1
  88. package/package.json +4 -4
  89. package/src/api/ClearSessionCache.ts +4 -0
  90. package/src/api/FirmwareUpdateV4.ts +15 -7
  91. package/src/api/GetDeviceState.ts +55 -0
  92. package/src/api/GetFeatures.ts +8 -3
  93. package/src/api/GetOnekeyFeatures.ts +3 -72
  94. package/src/api/GetPassphraseState.ts +3 -1
  95. package/src/api/SearchDevices.ts +25 -4
  96. package/src/api/device/DeviceRebootToBoardloader.ts +1 -0
  97. package/src/api/device/DeviceRebootToBootloader.ts +1 -0
  98. package/src/api/device/DeviceSettings.ts +28 -0
  99. package/src/api/index.ts +1 -4
  100. package/src/api/internal.ts +9 -0
  101. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -5
  102. package/src/api/protocol-v2/DeviceReboot.ts +3 -1
  103. package/src/api/protocol-v2/DeviceSettingsGet.ts +3 -1
  104. package/src/api/protocol-v2/DeviceSettingsSet.ts +2 -0
  105. package/src/api/protocol-v2/DeviceStatusGet.ts +1 -0
  106. package/src/api/utils.ts +9 -3
  107. package/src/constants/index.ts +1 -4
  108. package/src/core/index.ts +7 -1
  109. package/src/data/messages/messages-protocol-v2.json +22 -13
  110. package/src/device/Device.ts +256 -78
  111. package/src/device/DeviceFeaturesState.ts +34 -0
  112. package/src/device/DevicePool.ts +35 -9
  113. package/src/device/DeviceStateMapper.ts +364 -0
  114. package/src/device/DeviceStateProjector.ts +92 -0
  115. package/src/device/DeviceStateStore.ts +242 -0
  116. package/src/device/deviceIdentity.ts +18 -0
  117. package/src/deviceProfile/buildDeviceFeatures.ts +166 -109
  118. package/src/deviceProfile/index.ts +0 -1
  119. package/src/events/device.ts +13 -1
  120. package/src/events/logBlockEvent.ts +1 -1
  121. package/src/inject.ts +4 -5
  122. package/src/protocols/protocol-v2/features.ts +18 -9
  123. package/src/protocols/protocol-v2/index.ts +1 -0
  124. package/src/protocols/protocol-v2/walletSession.ts +5 -7
  125. package/src/types/api/deviceSettings.ts +8 -0
  126. package/src/types/api/getDeviceState.ts +19 -0
  127. package/src/types/api/index.ts +3 -20
  128. package/src/types/api/protocolV2.ts +0 -24
  129. package/src/types/device.ts +148 -1
  130. package/src/utils/deviceFeaturesUtils.ts +5 -1
  131. package/src/utils/deviceSettings.ts +41 -0
  132. package/src/utils/tracing.ts +3 -1
  133. package/dist/api/GetDeviceInfo.d.ts +0 -9
  134. package/dist/api/GetDeviceInfo.d.ts.map +0 -1
  135. package/dist/deviceProfile/buildDeviceProfile.d.ts +0 -22
  136. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +0 -1
  137. package/dist/types/api/getDeviceInfo.d.ts +0 -88
  138. package/dist/types/api/getDeviceInfo.d.ts.map +0 -1
  139. package/src/api/GetDeviceInfo.ts +0 -152
  140. package/src/deviceProfile/buildDeviceProfile.ts +0 -370
  141. package/src/types/api/getDeviceInfo.ts +0 -106
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from 'events';
2
2
  import semver from 'semver';
3
- import { Enum_Capability } from '@onekeyfe/hd-transport';
3
+ import { DeviceRebootType, Enum_Capability } from '@onekeyfe/hd-transport';
4
4
  import {
5
5
  EDeviceType,
6
6
  ERRORS,
@@ -32,10 +32,23 @@ import {
32
32
  import { generateInstanceId } from '../utils/tracing';
33
33
  // eslint-disable-next-line import/no-cycle
34
34
  import { DeviceCommands } from './DeviceCommands';
35
+ import { mergeDeviceFeaturesPatch } from './DeviceFeaturesState';
36
+ import {
37
+ mapDeviceSettingsToState,
38
+ mapFeaturesToState,
39
+ mapProtocolV1OnekeyFeaturesToState,
40
+ mapProtocolV2DeviceInfoToState,
41
+ mapProtocolV2DeviceStatusToState,
42
+ } from './DeviceStateMapper';
43
+ import { projectFeatures } from './DeviceStateProjector';
44
+ import { DeviceStateStore, createPublicDeviceState } from './DeviceStateStore';
35
45
  import { deviceWalletSessionStore } from './DeviceWalletSessionStore';
36
46
  import {
37
47
  type DeviceFirmwareRange,
38
48
  DeviceModelToTypes,
49
+ type DeviceStateEvent,
50
+ type DeviceStatePatch,
51
+ type DeviceStateUpdateSource,
39
52
  DeviceTypeToModels,
40
53
  type Device as DeviceTyped,
41
54
  EOneKeyDeviceMode,
@@ -52,12 +65,16 @@ import TransportManager from '../data-manager/TransportManager';
52
65
  import { toHardened } from '../api/helpers/pathUtils';
53
66
  import { existCapability } from '../utils/capabilitieUtils';
54
67
  import {
68
+ PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST,
69
+ PROTOCOL_V2_FULL_DEVICE_INFO_REQUEST,
70
+ PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
55
71
  requestProtocolV2DeviceInfo,
56
72
  requestProtocolV2DeviceStatus,
57
73
  } from '../protocols/protocol-v2/features';
58
- import { buildProtocolV1FeaturesPayload, buildProtocolV2FeaturesPayload } from '../deviceProfile';
74
+ import { buildProtocolV1FeaturesPayload } from '../deviceProfile';
59
75
 
60
76
  import type { PROTO } from '../constants';
77
+ import type { DeviceStateReadOptions } from '../types/api/getDeviceState';
61
78
  import type {
62
79
  DeviceButtonRequestPayload,
63
80
  DeviceFeaturesPayload,
@@ -80,6 +97,8 @@ export type InitOptions = {
80
97
  deriveCardano?: boolean;
81
98
  connectProtocol?: HardwareConnectProtocol;
82
99
  protocolV2DeviceInfoTimeoutMs?: number;
100
+ /** 设备发现内部使用:返回列表前刷新 Protocol V2 运行时状态。 */
101
+ refreshRuntimeState?: boolean;
83
102
  };
84
103
 
85
104
  export type RunOptions = {
@@ -100,6 +119,7 @@ export interface DeviceEvents {
100
119
  [DEVICE.ATTACH_PIN_ON_DEVICE]: [Device, PassphraseRequestPayload?];
101
120
  [DEVICE.BUTTON]: [Device, DeviceButtonRequestPayload];
102
121
  [DEVICE.FEATURES]: [Device, DeviceFeaturesPayload];
122
+ [DEVICE.STATE]: [Device, DeviceStateEvent];
103
123
  [DEVICE.PASSPHRASE]: [
104
124
  Device,
105
125
  PassphraseRequestPayload,
@@ -186,22 +206,26 @@ export class Device extends EventEmitter {
186
206
  */
187
207
  private deviceAcquired = false;
188
208
 
189
- /**
190
- * 唯一设备状态缓存。
191
- *
192
- * V1 直接保存原生 Features;V2 保存由 DeviceInfoGet 映射出的
193
- * Features 视图。Device 不再保存 profile,结构化 DeviceProfile 只作为
194
- * getDeviceInfo() 的 API 返回值存在。
195
- */
196
- features: Features | undefined = undefined;
209
+ /** 唯一设备状态缓存。旧 Features 仅通过 getter/setter 做兼容投影。 */
210
+ private stateStore = new DeviceStateStore();
197
211
 
198
- /**
199
- * 是否需要更新设备信息。
200
- *
201
- * 历史名称保留用于兼容现有调用语义;对 V2 表示 features 需要由
202
- * DeviceInfoGet 刷新。
203
- */
204
- featuresNeedsReload = false;
212
+ /** 物理重连或重启后,下一次初始化必须重新读取 DeviceInfo。 */
213
+ private protocolV2StateNeedsReload = false;
214
+
215
+ get state() {
216
+ return this.stateStore.getState();
217
+ }
218
+
219
+ get features(): Features | undefined {
220
+ return this.state ? projectFeatures(this.state) : undefined;
221
+ }
222
+
223
+ set features(features: Features | undefined) {
224
+ this.stateStore = new DeviceStateStore();
225
+ if (features) {
226
+ this.stateStore.update(mapFeaturesToState(features), 'compatibility');
227
+ }
228
+ }
205
229
 
206
230
  runPromise?: Deferred<void> | null;
207
231
 
@@ -267,6 +291,7 @@ export class Device extends EventEmitter {
267
291
  const deviceId = this.getCurrentDeviceId() || null;
268
292
 
269
293
  const { features } = this;
294
+ const state = this.state ? createPublicDeviceState(this.state) : undefined;
270
295
 
271
296
  return {
272
297
  /** Android uses Mac address, iOS uses uuid, USB uses uuid */
@@ -283,9 +308,11 @@ export class Device extends EventEmitter {
283
308
  path: this.originalDescriptor?.path,
284
309
  bleName,
285
310
  name: bleName || label || `OneKey ${deviceType?.toUpperCase()}`,
311
+ displayName: label || bleName || `OneKey ${deviceType?.toUpperCase()}`,
286
312
  label: label || 'OneKey',
287
313
  mode: this.getMode(),
288
314
  features,
315
+ state,
289
316
  sessionId: this.features?.sessionId ?? null,
290
317
  firmwareVersion: this.getFirmwareVersion(),
291
318
  bleFirmwareVersion: this.getBLEFirmwareVersion(),
@@ -725,15 +752,16 @@ export class Device extends EventEmitter {
725
752
  // Protocol V2 不支持传统 Initialize,直接使用协议专用初始化流程。
726
753
  if (this.isProtocolV2()) {
727
754
  this.passphraseState = options?.passphraseState;
728
- if (this.features && !this.featuresNeedsReload && !options?.initSession) {
729
- if (this.features.bootloaderMode) {
755
+ if (this.state && !options?.initSession && !this.protocolV2StateNeedsReload) {
756
+ if (this.features?.bootloaderMode) {
730
757
  return;
731
758
  }
732
- // 普通业务调用复用缓存。动态状态由显式 getFeatures/deviceStatusGet
759
+ // 普通业务调用复用缓存。动态状态由显式 getDeviceState refresh
733
760
  // 解锁流程和设备事件刷新,避免每个 SDK 方法都额外轮询 DeviceStatus。
734
761
  return;
735
762
  }
736
763
  await this._initializeProtocolV2(options);
764
+ this.protocolV2StateNeedsReload = false;
737
765
  return;
738
766
  }
739
767
 
@@ -797,17 +825,10 @@ export class Device extends EventEmitter {
797
825
  commands: this.commands,
798
826
  timeoutMs: options?.protocolV2DeviceInfoTimeoutMs,
799
827
  });
800
- const deviceStatus = await requestProtocolV2DeviceStatus({
801
- commands: this.commands,
802
- }).catch(error => {
803
- Log.debug('Protocol V2 status unavailable during initialization:', error);
804
- return undefined;
805
- });
806
828
  // 默认请求不含 SE/hash 数据,scope 如实标注为 basic;
807
- // 完整数据由 getDeviceInfo(scope:'verify'|'full') 获取。
808
- const features = this.updateProtocolV2Features(deviceInfo, deviceStatus);
829
+ // 完整版本与校验数据由公共 getDeviceState({ scope: 'firmware' }) 显式获取。
830
+ const features = this.updateProtocolV2Features(deviceInfo, null);
809
831
  Log.debug('Protocol V2 features:', features);
810
- this.featuresNeedsReload = false;
811
832
  } catch (error) {
812
833
  Log.error('Protocol V2 initialization failed:', error);
813
834
  throw error;
@@ -819,10 +840,7 @@ export class Device extends EventEmitter {
819
840
  const deviceInfo = await requestProtocolV2DeviceInfo({
820
841
  commands: this.commands,
821
842
  });
822
- const deviceStatus = await requestProtocolV2DeviceStatus({
823
- commands: this.commands,
824
- }).catch(() => undefined);
825
- return this.updateProtocolV2Features(deviceInfo, deviceStatus);
843
+ return this.updateProtocolV2Features(deviceInfo, null);
826
844
  }
827
845
 
828
846
  const { message } = await this.commands.typedCall('GetFeatures', 'Features', {});
@@ -830,6 +848,70 @@ export class Device extends EventEmitter {
830
848
  return this.features;
831
849
  }
832
850
 
851
+ async getDeviceState(params: DeviceStateReadOptions = {}) {
852
+ const refresh = new Set(params.refreshSections ?? []);
853
+ const getProtocolV2DeviceInfoRequest = () => {
854
+ if (refresh.has('verification')) return PROTOCOL_V2_FULL_DEVICE_INFO_REQUEST;
855
+ if (refresh.has('versions')) return PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST;
856
+ return PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST;
857
+ };
858
+ let initializedWithDeviceInfo = false;
859
+
860
+ if (!this.state) {
861
+ if (this.isProtocolV2()) {
862
+ const deviceInfo = await requestProtocolV2DeviceInfo({
863
+ commands: this.commands,
864
+ request: getProtocolV2DeviceInfoRequest(),
865
+ });
866
+ this.updateState(mapProtocolV2DeviceInfoToState(deviceInfo), 'device-info');
867
+ initializedWithDeviceInfo = true;
868
+ } else {
869
+ await this.getFeatures();
870
+ }
871
+ } else if (!this.isProtocolV2() && refresh.size > 0) {
872
+ await this.getFeatures();
873
+ }
874
+
875
+ if (!this.isProtocolV2() && refresh.has('verification')) {
876
+ const { message } = await this.commands.typedCall('OnekeyGetFeatures', 'OnekeyFeatures');
877
+ this.updateState(mapProtocolV1OnekeyFeaturesToState(message), 'device-info');
878
+ }
879
+
880
+ if (this.isProtocolV2()) {
881
+ const refreshDeviceInfo =
882
+ refresh.has('identity') || refresh.has('versions') || refresh.has('verification');
883
+ if (refreshDeviceInfo && !initializedWithDeviceInfo) {
884
+ const deviceInfo = await requestProtocolV2DeviceInfo({
885
+ commands: this.commands,
886
+ request: getProtocolV2DeviceInfoRequest(),
887
+ });
888
+ this.updateState(
889
+ mapProtocolV2DeviceInfoToState(deviceInfo, this.state?.status.mode),
890
+ 'device-info'
891
+ );
892
+ }
893
+
894
+ if (refresh.has('status') && this.state?.status.mode === 'normal') {
895
+ const status = await requestProtocolV2DeviceStatus({ commands: this.commands });
896
+ this.updateState(mapProtocolV2DeviceStatusToState(status), 'device-status');
897
+ }
898
+
899
+ if (refresh.has('settings') && this.state?.status.mode === 'normal') {
900
+ const { message } = await this.commands.typedCall(
901
+ 'DeviceSettingsGet',
902
+ 'DeviceSettings',
903
+ {}
904
+ );
905
+ this.updateState(mapDeviceSettingsToState(message), 'apply-settings');
906
+ }
907
+ }
908
+
909
+ if (!this.state) {
910
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed);
911
+ }
912
+ return params.includeRaw ? structuredClone(this.state) : createPublicDeviceState(this.state);
913
+ }
914
+
833
915
  _updateFeatures(protoFeatures: PROTO.Features | Features, initSession?: boolean) {
834
916
  const previousCacheDeviceKey = this.getSessionCacheDeviceKey();
835
917
  let feat =
@@ -845,7 +927,7 @@ export class Device extends EventEmitter {
845
927
 
846
928
  feat = fixFeaturesFirmwareVersion(feat);
847
929
 
848
- this.features = feat;
930
+ this.updateState(mapFeaturesToState(feat), 'initialize');
849
931
  const nextCacheDeviceKey = this.getSessionCacheDeviceKey();
850
932
  if (previousCacheDeviceKey && nextCacheDeviceKey) {
851
933
  deviceWalletSessionStore.migrateDeviceKey(previousCacheDeviceKey, nextCacheDeviceKey);
@@ -853,42 +935,131 @@ export class Device extends EventEmitter {
853
935
  if (feat.deviceId && feat.sessionId) {
854
936
  this.setInternalState(feat.sessionId, initSession);
855
937
  }
856
- this.featuresNeedsReload = false;
857
- this.emit(DEVICE.FEATURES, this, feat);
938
+ }
939
+
940
+ updateState(patch: DeviceStatePatch, source: DeviceStateUpdateSource) {
941
+ const result = this.stateStore.update(patch, source);
942
+ if (result.changedKeys.length === 0) return result.state;
943
+
944
+ const event: DeviceStateEvent = {
945
+ connectId: this.getConnectId() ?? null,
946
+ state: createPublicDeviceState(result.state),
947
+ revision: result.revision,
948
+ source,
949
+ changedKeys: result.changedKeys,
950
+ };
951
+ Log.debug('Device state patch committed', {
952
+ source,
953
+ keys: result.changedKeys,
954
+ });
955
+ this.emit(DEVICE.STATE, this, event);
956
+ if (result.state.protocol === 'V1') {
957
+ this.emit(DEVICE.FEATURES, this, projectFeatures(result.state));
958
+ }
959
+ return result.state;
960
+ }
961
+
962
+ clearCachedSession(deviceId?: string, passphraseState?: string) {
963
+ const current = this.state;
964
+ if (!current?.session) return false;
965
+ if (deviceId && current.identity.deviceId !== deviceId) return false;
966
+ if (passphraseState && current.session.passphraseState !== passphraseState) return false;
967
+ return this.stateStore.clearSession('session-clear') !== undefined;
968
+ }
969
+
970
+ updateFeaturesPatch(patch: Partial<Features>, source: DeviceStateUpdateSource) {
971
+ const currentFeatures = this.features;
972
+ if (!currentFeatures) return undefined;
973
+
974
+ const features = mergeDeviceFeaturesPatch(currentFeatures, patch);
975
+ if (features === currentFeatures) return currentFeatures;
976
+
977
+ const normalized = fixFeaturesFirmwareVersion(features);
978
+ this.updateState(mapFeaturesToState(normalized), source);
979
+ return this.features;
858
980
  }
859
981
 
860
982
  updateProtocolV2Features(deviceInfo?: ProtocolV2DeviceInfo, deviceStatus?: DeviceStatus | null) {
861
983
  const previousCacheDeviceKey = this.getSessionCacheDeviceKey();
862
- const resolvedDeviceStatus =
863
- deviceStatus === undefined
864
- ? this.features?.raw?.protocolV2DeviceStatus
865
- : deviceStatus ?? undefined;
866
- const features = fixFeaturesFirmwareVersion(
867
- buildProtocolV2FeaturesPayload({
868
- deviceInfo,
869
- deviceStatus: resolvedDeviceStatus,
870
- previous: this.features,
871
- })
872
- );
873
- this.features = features;
984
+ if (deviceStatus) {
985
+ this.updateState(mapProtocolV2DeviceStatusToState(deviceStatus), 'device-status');
986
+ }
987
+ if (deviceInfo) {
988
+ this.updateState(
989
+ mapProtocolV2DeviceInfoToState(deviceInfo, this.state?.status.mode),
990
+ 'device-info'
991
+ );
992
+ }
874
993
  const nextCacheDeviceKey = this.getSessionCacheDeviceKey();
875
994
  if (previousCacheDeviceKey && nextCacheDeviceKey) {
876
995
  deviceWalletSessionStore.migrateDeviceKey(previousCacheDeviceKey, nextCacheDeviceKey);
877
996
  }
878
- this.featuresNeedsReload = false;
879
- this.emit(DEVICE.FEATURES, this, features);
880
- return features;
997
+ return this.features as Features;
881
998
  }
882
999
 
883
1000
  updateProtocolV2Status(status: DeviceStatus) {
884
- const previousDeviceInfo = this.features?.raw?.protocolV2DeviceInfo;
885
- const previousStatus = this.features?.raw?.protocolV2DeviceStatus;
1001
+ const previousDeviceInfo = this.state?.raw?.protocolV2DeviceInfo;
1002
+ const previousStatus = this.state?.raw?.protocolV2DeviceStatus;
886
1003
  return this.updateProtocolV2Features(previousDeviceInfo, {
887
1004
  ...previousStatus,
888
1005
  ...status,
889
1006
  });
890
1007
  }
891
1008
 
1009
+ markTransportDisconnected() {
1010
+ if (!this.isProtocolV2()) return;
1011
+ this.protocolV2StateNeedsReload = true;
1012
+ this.clearPreInitialized();
1013
+ this.clearCachedSession();
1014
+ }
1015
+
1016
+ markProtocolV2Reboot(rebootType: DeviceRebootType) {
1017
+ if (!this.isProtocolV2()) return;
1018
+
1019
+ this.protocolV2StateNeedsReload = true;
1020
+ this.clearPreInitialized();
1021
+ let loaderMode: 'bootloader' | 'romloader' | undefined;
1022
+ if (rebootType === DeviceRebootType.Bootloader) {
1023
+ loaderMode = 'bootloader';
1024
+ } else if (rebootType === DeviceRebootType.Romloader) {
1025
+ loaderMode = 'romloader';
1026
+ }
1027
+ if (loaderMode) {
1028
+ this.updateState(
1029
+ {
1030
+ identity: { deviceId: null },
1031
+ status: {
1032
+ mode: loaderMode,
1033
+ initialized: null,
1034
+ unlocked: null,
1035
+ firmwarePresent: null,
1036
+ backupRequired: null,
1037
+ noBackup: null,
1038
+ unfinishedBackup: null,
1039
+ recoveryMode: null,
1040
+ passphraseProtection: null,
1041
+ pinProtection: null,
1042
+ attachToPinEnabled: null,
1043
+ unlockedAttachPin: null,
1044
+ },
1045
+ session: null,
1046
+ raw: { protocolV2DeviceStatus: null },
1047
+ },
1048
+ 'transport-reconnect'
1049
+ );
1050
+ return;
1051
+ }
1052
+
1053
+ this.updateState(
1054
+ {
1055
+ status: { mode: 'normal', unlocked: null },
1056
+ session: null,
1057
+ raw: { protocolV2DeviceStatus: null },
1058
+ },
1059
+ 'transport-reconnect'
1060
+ );
1061
+ }
1062
+
892
1063
  /**
893
1064
  * 暂时只在 acquire 后更新 Session ID
894
1065
  * 后续看是否有需要依据 listen 返回结果更新
@@ -955,6 +1126,7 @@ export class Device extends EventEmitter {
955
1126
  }
956
1127
  }
957
1128
  } catch (error) {
1129
+ await this.release();
958
1130
  this.runPromise = null;
959
1131
  if (error instanceof HardwareError) {
960
1132
  return Promise.reject(error);
@@ -1055,22 +1227,23 @@ export class Device extends EventEmitter {
1055
1227
  }
1056
1228
 
1057
1229
  getMode() {
1058
- if (this.features?.bootloaderMode) {
1059
- // bootloader mode
1060
- return EOneKeyDeviceMode.bootloader;
1061
- }
1062
-
1063
- if (!this.features?.initialized) {
1064
- // not initialized
1065
- return EOneKeyDeviceMode.notInitialized;
1230
+ switch (this.state?.status.mode) {
1231
+ case 'bootloader':
1232
+ case 'romloader':
1233
+ return EOneKeyDeviceMode.bootloader;
1234
+ case 'notInitialized':
1235
+ return EOneKeyDeviceMode.notInitialized;
1236
+ case 'backupMode':
1237
+ return EOneKeyDeviceMode.backupMode;
1238
+ case 'normal':
1239
+ return EOneKeyDeviceMode.normal;
1240
+ default:
1241
+ break;
1066
1242
  }
1067
1243
 
1068
- if (this.features?.noBackup) {
1069
- // backup mode
1070
- return EOneKeyDeviceMode.backupMode;
1071
- }
1072
-
1073
- // normal mode
1244
+ if (this.features?.bootloaderMode) return EOneKeyDeviceMode.bootloader;
1245
+ if (this.features?.initialized === false) return EOneKeyDeviceMode.notInitialized;
1246
+ if (this.features?.noBackup) return EOneKeyDeviceMode.backupMode;
1074
1247
  return EOneKeyDeviceMode.normal;
1075
1248
  }
1076
1249
 
@@ -1165,6 +1338,7 @@ export class Device extends EventEmitter {
1165
1338
 
1166
1339
  async lockDevice(): Promise<Success> {
1167
1340
  const res = await this.commands.typedCall('LockDevice', 'Success', {});
1341
+ this.updateState({ status: { unlocked: false } }, 'lock');
1168
1342
  return res.message;
1169
1343
  }
1170
1344
 
@@ -1194,12 +1368,7 @@ export class Device extends EventEmitter {
1194
1368
  throw error;
1195
1369
  }
1196
1370
 
1197
- const { message: status } = await this.commands.typedCall(
1198
- 'DeviceStatusGet',
1199
- 'DeviceStatus',
1200
- {}
1201
- );
1202
- return this.updateProtocolV2Status(status);
1371
+ return this.updateFeaturesPatch({ unlocked: true }, 'unlock');
1203
1372
  }
1204
1373
 
1205
1374
  const firmwareVersion = this.getCurrentFirmwareVersionString() ?? '0.0.0';
@@ -1220,11 +1389,20 @@ export class Device extends EventEmitter {
1220
1389
  if (supportUnlock) {
1221
1390
  const res = await this.commands.typedCall('UnLockDevice', 'UnLockDeviceResponse');
1222
1391
  if (this.features) {
1223
- this.features.unlocked = res.message.unlocked == null ? null : res.message.unlocked;
1224
- this.features.unlockedAttachPin =
1225
- res.message.unlocked_attach_pin == null ? undefined : res.message.unlocked_attach_pin;
1226
- this.features.passphraseProtection =
1227
- res.message.passphrase_protection == null ? null : res.message.passphrase_protection;
1392
+ this.updateState(
1393
+ {
1394
+ status: {
1395
+ unlocked: res.message.unlocked == null ? null : res.message.unlocked,
1396
+ unlockedAttachPin:
1397
+ res.message.unlocked_attach_pin == null ? null : res.message.unlocked_attach_pin,
1398
+ passphraseProtection:
1399
+ res.message.passphrase_protection == null
1400
+ ? null
1401
+ : res.message.passphrase_protection,
1402
+ },
1403
+ },
1404
+ 'unlock'
1405
+ );
1228
1406
 
1229
1407
  return Promise.resolve(this.features);
1230
1408
  }
@@ -0,0 +1,34 @@
1
+ import type { Features } from '../types';
2
+
3
+ export type DeviceFeaturesUpdateSource =
4
+ | 'device-info'
5
+ | 'device-status'
6
+ | 'device-settings-get'
7
+ | 'device-settings-set'
8
+ | 'apply-settings'
9
+ | 'unlock'
10
+ | 'cache';
11
+
12
+ /**
13
+ * 合并设备状态的字段级 patch。
14
+ *
15
+ * `undefined` 表示本次响应未提供该字段,不覆盖缓存;`null` 表示设备明确
16
+ * 返回空值,需要写入缓存。raw 等对象字段由调用方构造完整值后再传入,
17
+ * 这里不做隐式深合并。
18
+ */
19
+ export function mergeDeviceFeaturesPatch(previous: Features, patch: Partial<Features>): Features {
20
+ let next = previous;
21
+
22
+ for (const [key, value] of Object.entries(patch) as Array<
23
+ [keyof Features, Features[keyof Features] | undefined]
24
+ >) {
25
+ if (value !== undefined && !Object.is(previous[key], value)) {
26
+ if (next === previous) {
27
+ next = { ...previous };
28
+ }
29
+ (next as Record<keyof Features, Features[keyof Features]>)[key] = value;
30
+ }
31
+ }
32
+
33
+ return next;
34
+ }
@@ -106,6 +106,7 @@ export class DevicePool extends EventEmitter {
106
106
  if (exist) {
107
107
  // Log.debug('find existed Device: ', connectId);
108
108
  device.updateDescriptor(exist, true);
109
+ await this._refreshRuntimeState(device, initOptions);
109
110
  devices[connectId] = device;
110
111
  deviceList.push(device);
111
112
  await this._checkDevicePool(initOptions, connectId);
@@ -156,12 +157,36 @@ export class DevicePool extends EventEmitter {
156
157
  device = Device.fromDescriptor(descriptor);
157
158
  device.deviceConnector = this.connector;
158
159
  await device.connect(initOptions?.connectProtocol);
159
- await device.initialize(initOptions);
160
- await device.release();
160
+ try {
161
+ await device.initialize(initOptions);
162
+ if (initOptions?.refreshRuntimeState && device.isProtocolV2()) {
163
+ await device.getDeviceState({ refreshSections: ['status'] });
164
+ }
165
+ } finally {
166
+ await device.release();
167
+ }
161
168
  }
162
169
  return device;
163
170
  }
164
171
 
172
+ static async _refreshRuntimeState(device: Device, initOptions?: InitOptions) {
173
+ if (!initOptions?.refreshRuntimeState || !device.isProtocolV2()) return;
174
+ let refreshError: unknown;
175
+ await device.run(
176
+ async () => {
177
+ try {
178
+ await device.getDeviceState({ refreshSections: ['status'] });
179
+ } catch (error) {
180
+ // Device.run 在回调正常结束后统一 release;随后再把真实读取错误交给调用方。
181
+ refreshError = error;
182
+ }
183
+ },
184
+ { connectProtocol: initOptions.connectProtocol }
185
+ );
186
+ if (refreshError instanceof Error) throw refreshError;
187
+ if (refreshError) throw new Error(String(refreshError));
188
+ }
189
+
165
190
  static async _checkDevicePool(initOptions?: InitOptions, connectId?: string) {
166
191
  await this._sendConnectMessage(initOptions, connectId);
167
192
  this._sendDisconnectMessage();
@@ -170,21 +195,21 @@ export class DevicePool extends EventEmitter {
170
195
  static async _sendConnectMessage(initOptions?: InitOptions, connectId?: string) {
171
196
  for (let i = this.connectedPool.length - 1; i >= 0; i--) {
172
197
  const descriptor = this.connectedPool[i];
173
- if (connectId && descriptor.path !== connectId) {
174
- continue;
198
+ if (!connectId || descriptor.path === connectId) {
199
+ const device = await this._createDevice(descriptor, initOptions);
200
+ Log.debug('emit DEVICE.CONNECT: ', device?.features);
201
+ this.emitter.emit(DEVICE.CONNECT, device);
202
+ this.connectedPool.splice(i, 1);
175
203
  }
176
- const device = await this._createDevice(descriptor, initOptions);
177
- Log.debug('emit DEVICE.CONNECT: ', device?.features);
178
- this.emitter.emit(DEVICE.CONNECT, device);
179
- this.connectedPool.splice(i, 1);
180
204
  }
181
205
  }
182
206
 
183
207
  static _sendDisconnectMessage() {
184
208
  for (let i = this.disconnectPool.length - 1; i >= 0; i--) {
185
- const descriptor = this.connectedPool[i];
209
+ const descriptor = this.disconnectPool[i];
186
210
  const device = descriptor?.path ? this.getDeviceByPath(descriptor.path) : null;
187
211
  if (device) {
212
+ device.markTransportDisconnected();
188
213
  this.emitter.emit(DEVICE.DISCONNECT, device);
189
214
  }
190
215
  this.disconnectPool.splice(i, 1);
@@ -224,6 +249,7 @@ export class DevicePool extends EventEmitter {
224
249
  }
225
250
 
226
251
  Log.debug('emit DEVICE.DISCONNECT: ', device.features);
252
+ device.markTransportDisconnected();
227
253
  this.emitter.emit(DEVICE.DISCONNECT, device);
228
254
  });
229
255
  }