@onekeyfe/hd-core 1.1.27-alpha.42 → 1.1.27-alpha.45

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 (153) hide show
  1. package/__tests__/DeviceCommands.test.ts +99 -0
  2. package/__tests__/evmLedgerLegacySafety.test.ts +261 -0
  3. package/__tests__/preInitialize.test.ts +22 -0
  4. package/__tests__/protocol-v2.test.ts +233 -20
  5. package/dist/api/BaseMethod.d.ts +7 -1
  6. package/dist/api/BaseMethod.d.ts.map +1 -1
  7. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  8. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  9. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  10. package/dist/api/alephium/AlephiumSignMessage.d.ts.map +1 -1
  11. package/dist/api/alephium/AlephiumSignTransaction.d.ts.map +1 -1
  12. package/dist/api/algo/AlgoSignTransaction.d.ts.map +1 -1
  13. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  14. package/dist/api/aptos/AptosSignInMessage.d.ts.map +1 -1
  15. package/dist/api/aptos/AptosSignMessage.d.ts.map +1 -1
  16. package/dist/api/aptos/AptosSignTransaction.d.ts.map +1 -1
  17. package/dist/api/benfen/BenfenSignMessage.d.ts.map +1 -1
  18. package/dist/api/benfen/BenfenSignTransaction.d.ts.map +1 -1
  19. package/dist/api/btc/BTCSignMessage.d.ts.map +1 -1
  20. package/dist/api/btc/BTCSignPsbt.d.ts.map +1 -1
  21. package/dist/api/btc/BTCSignTransaction.d.ts.map +1 -1
  22. package/dist/api/cardano/CardanoSignMessage.d.ts.map +1 -1
  23. package/dist/api/cardano/CardanoSignTransaction.d.ts.map +1 -1
  24. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  25. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  26. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  27. package/dist/api/cosmos/CosmosSignTransaction.d.ts.map +1 -1
  28. package/dist/api/device/PreInitialize.d.ts +6 -0
  29. package/dist/api/device/PreInitialize.d.ts.map +1 -0
  30. package/dist/api/dynex/DnxSignTransaction.d.ts.map +1 -1
  31. package/dist/api/evm/EVMSignMessage.d.ts.map +1 -1
  32. package/dist/api/evm/EVMSignMessageEIP712.d.ts.map +1 -1
  33. package/dist/api/evm/EVMSignTransaction.d.ts.map +1 -1
  34. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  35. package/dist/api/filecoin/FilecoinSignTransaction.d.ts.map +1 -1
  36. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -10
  37. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  38. package/dist/api/index.d.ts +1 -0
  39. package/dist/api/index.d.ts.map +1 -1
  40. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  41. package/dist/api/near/NearSignTransaction.d.ts.map +1 -1
  42. package/dist/api/nem/NEMSignTransaction.d.ts.map +1 -1
  43. package/dist/api/neo/NeoSignTransaction.d.ts.map +1 -1
  44. package/dist/api/nervos/NervosSignTransaction.d.ts.map +1 -1
  45. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  46. package/dist/api/nostr/NostrSignEvent.d.ts.map +1 -1
  47. package/dist/api/nostr/NostrSignSchnorr.d.ts.map +1 -1
  48. package/dist/api/polkadot/PolkadotSignTransaction.d.ts.map +1 -1
  49. package/dist/api/protocol-v2/FilesystemDiskControl.d.ts +1 -1
  50. package/dist/api/scdo/ScdoSignMessage.d.ts.map +1 -1
  51. package/dist/api/scdo/ScdoSignTransaction.d.ts.map +1 -1
  52. package/dist/api/solana/SolSignMessage.d.ts +0 -1
  53. package/dist/api/solana/SolSignMessage.d.ts.map +1 -1
  54. package/dist/api/solana/SolSignOffchainMessage.d.ts +0 -1
  55. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  56. package/dist/api/solana/SolSignTransaction.d.ts +0 -2
  57. package/dist/api/solana/SolSignTransaction.d.ts.map +1 -1
  58. package/dist/api/starcoin/StarcoinSignMessage.d.ts.map +1 -1
  59. package/dist/api/starcoin/StarcoinSignTransaction.d.ts.map +1 -1
  60. package/dist/api/stellar/StellarSignTransaction.d.ts.map +1 -1
  61. package/dist/api/sui/SuiSignMessage.d.ts.map +1 -1
  62. package/dist/api/sui/SuiSignTransaction.d.ts.map +1 -1
  63. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  64. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  65. package/dist/api/ton/TonSignProof.d.ts.map +1 -1
  66. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  67. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  68. package/dist/api/xrp/XrpSignTransaction.d.ts.map +1 -1
  69. package/dist/core/PollingStateManager.d.ts +8 -0
  70. package/dist/core/PollingStateManager.d.ts.map +1 -0
  71. package/dist/core/RequestQueue.d.ts +1 -1
  72. package/dist/core/RequestQueue.d.ts.map +1 -1
  73. package/dist/core/index.d.ts.map +1 -1
  74. package/dist/device/Device.d.ts +17 -1
  75. package/dist/device/Device.d.ts.map +1 -1
  76. package/dist/index.d.ts +22 -2
  77. package/dist/index.js +454 -195
  78. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  79. package/dist/types/api/index.d.ts +2 -0
  80. package/dist/types/api/index.d.ts.map +1 -1
  81. package/dist/types/api/preInitialize.d.ts +3 -0
  82. package/dist/types/api/preInitialize.d.ts.map +1 -0
  83. package/dist/types/params.d.ts +1 -0
  84. package/dist/types/params.d.ts.map +1 -1
  85. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  86. package/dist/utils/patch.d.ts +1 -1
  87. package/package.json +4 -4
  88. package/src/api/BaseMethod.ts +82 -2
  89. package/src/api/FirmwareUpdateV3.ts +0 -4
  90. package/src/api/FirmwareUpdateV4.ts +1 -19
  91. package/src/api/GetPassphraseState.ts +4 -3
  92. package/src/api/alephium/AlephiumSignMessage.ts +1 -0
  93. package/src/api/alephium/AlephiumSignTransaction.ts +1 -0
  94. package/src/api/algo/AlgoSignTransaction.ts +1 -0
  95. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +8 -0
  96. package/src/api/aptos/AptosSignInMessage.ts +1 -0
  97. package/src/api/aptos/AptosSignMessage.ts +1 -0
  98. package/src/api/aptos/AptosSignTransaction.ts +1 -0
  99. package/src/api/benfen/BenfenSignMessage.ts +1 -0
  100. package/src/api/benfen/BenfenSignTransaction.ts +1 -0
  101. package/src/api/btc/BTCSignMessage.ts +1 -0
  102. package/src/api/btc/BTCSignPsbt.ts +1 -0
  103. package/src/api/btc/BTCSignTransaction.ts +1 -0
  104. package/src/api/cardano/CardanoSignMessage.ts +1 -0
  105. package/src/api/cardano/CardanoSignTransaction.ts +1 -0
  106. package/src/api/conflux/ConfluxSignMessage.ts +1 -0
  107. package/src/api/conflux/ConfluxSignMessageCIP23.ts +1 -0
  108. package/src/api/conflux/ConfluxSignTransaction.ts +1 -0
  109. package/src/api/cosmos/CosmosSignTransaction.ts +1 -0
  110. package/src/api/device/PreInitialize.ts +41 -0
  111. package/src/api/dynex/DnxSignTransaction.ts +1 -0
  112. package/src/api/evm/EVMSignMessage.ts +2 -0
  113. package/src/api/evm/EVMSignMessageEIP712.ts +1 -0
  114. package/src/api/evm/EVMSignTransaction.ts +2 -0
  115. package/src/api/evm/EVMSignTypedData.ts +2 -0
  116. package/src/api/filecoin/FilecoinSignTransaction.ts +1 -0
  117. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +4 -27
  118. package/src/api/index.ts +1 -0
  119. package/src/api/kaspa/KaspaSignTransaction.ts +1 -0
  120. package/src/api/near/NearSignTransaction.ts +1 -0
  121. package/src/api/nem/NEMSignTransaction.ts +1 -0
  122. package/src/api/neo/NeoSignTransaction.ts +1 -0
  123. package/src/api/nervos/NervosSignTransaction.ts +1 -0
  124. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  125. package/src/api/nostr/NostrSignEvent.ts +1 -0
  126. package/src/api/nostr/NostrSignSchnorr.ts +1 -0
  127. package/src/api/polkadot/PolkadotSignTransaction.ts +1 -0
  128. package/src/api/scdo/ScdoSignMessage.ts +1 -0
  129. package/src/api/scdo/ScdoSignTransaction.ts +1 -0
  130. package/src/api/solana/SolSignMessage.ts +1 -1
  131. package/src/api/solana/SolSignOffchainMessage.ts +1 -1
  132. package/src/api/solana/SolSignTransaction.ts +1 -2
  133. package/src/api/starcoin/StarcoinSignMessage.ts +1 -0
  134. package/src/api/starcoin/StarcoinSignTransaction.ts +1 -0
  135. package/src/api/stellar/StellarSignTransaction.ts +1 -0
  136. package/src/api/sui/SuiSignMessage.ts +1 -0
  137. package/src/api/sui/SuiSignTransaction.ts +1 -0
  138. package/src/api/ton/TonSignData.ts +1 -0
  139. package/src/api/ton/TonSignMessage.ts +1 -0
  140. package/src/api/ton/TonSignProof.ts +1 -0
  141. package/src/api/tron/TronSignMessage.ts +1 -0
  142. package/src/api/tron/TronSignTransaction.ts +1 -0
  143. package/src/api/xrp/XrpSignTransaction.ts +1 -0
  144. package/src/core/PollingStateManager.ts +47 -0
  145. package/src/core/RequestQueue.ts +10 -3
  146. package/src/core/index.ts +153 -34
  147. package/src/device/Device.ts +91 -29
  148. package/src/inject.ts +1 -1
  149. package/src/protocols/protocol-v2/features.ts +5 -6
  150. package/src/types/api/index.ts +2 -0
  151. package/src/types/api/preInitialize.ts +3 -0
  152. package/src/types/params.ts +5 -0
  153. package/src/utils/deviceFeaturesUtils.ts +8 -17
package/src/core/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
3
  import {
4
- EDeviceType,
5
4
  ERRORS,
6
5
  ERROR_CODES_REQUIRE_RELEASE,
7
6
  HardwareError,
@@ -20,7 +19,6 @@ import {
20
19
  enableLog,
21
20
  getDeviceBLEFirmwareVersion,
22
21
  getDeviceFirmwareVersion,
23
- getDeviceType,
24
22
  getFirmwareType,
25
23
  getLogger,
26
24
  getMethodVersionRange,
@@ -45,6 +43,7 @@ import {
45
43
  import { Device } from '../device/Device';
46
44
  import { DeviceList } from '../device/DeviceList';
47
45
  import { DevicePool } from '../device/DevicePool';
46
+ import { PollingStateManager } from './PollingStateManager';
48
47
  import { findMethod } from '../api/utils';
49
48
  import { DataManager } from '../data-manager';
50
49
  import { UI_REQUEST as UI_REQUEST_CONST } from '../constants/ui-request';
@@ -76,6 +75,12 @@ import type {
76
75
  import type { BaseMethod } from '../api/BaseMethod';
77
76
 
78
77
  const Log = getLogger(LoggerNames.Core);
78
+ const PRE_INITIALIZE_TTL_MS = 60 * 1000;
79
+
80
+ // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
81
+ // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
82
+ const preWarmInflight = new Map<string, Promise<any>>();
83
+ const preWarmDoneAt = new Map<string, number>();
79
84
 
80
85
  export type CoreContext = ReturnType<Core['getCoreContext']>;
81
86
 
@@ -107,8 +112,7 @@ let _connector: DeviceConnector | undefined;
107
112
  let _uiPromises: UiPromise<UiPromiseResponse['type']>[] = []; // Waiting for ui response
108
113
 
109
114
  const deviceCacheMap = new Map<string, Device>();
110
- let pollingId = 1;
111
- const pollingState: Record<number, boolean> = {};
115
+ const pollingManager = new PollingStateManager();
112
116
 
113
117
  let preConnectCache: {
114
118
  passphraseState: string | undefined;
@@ -210,9 +214,63 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
210
214
  return createResponseMessage(method.responseID, false, { error });
211
215
  }
212
216
 
217
+ // only the pre-warm signal (PreInitialize) forks here; normal methods fall
218
+ // through to onCallDevice below, so the pre-warm dedup/guards never touch them
219
+ if (method.isPreWarmSignal) {
220
+ return handlePreWarmSignal(context, message, method);
221
+ }
222
+
213
223
  return onCallDevice(context, message, method);
214
224
  };
215
225
 
226
+ // Wrapper for "pre-warm signal" methods: coalesce in-flight same-key pre-warm,
227
+ // skip if warmed within TTL, else run + track. The "hang up so the next real
228
+ // call waits" part lives in onCallDevice (setPrePendingCallPromise).
229
+ const handlePreWarmSignal = async (
230
+ context: CoreContext,
231
+ message: CoreMessage,
232
+ method: BaseMethod
233
+ ): Promise<any> => {
234
+ // no connectId: can't target a device safely, skip pre-warm (ack only)
235
+ if (!method.connectId) {
236
+ return createResponseMessage(method.responseID, true, true);
237
+ }
238
+
239
+ const key = method.getPreWarmKey();
240
+
241
+ const inflight = preWarmInflight.get(key);
242
+ if (inflight) {
243
+ // reply with THIS call's responseID (not the other call's response object)
244
+ try {
245
+ await inflight;
246
+ } catch {
247
+ // pre-warm is best-effort; ignore its failure for the coalesced caller
248
+ }
249
+ return createResponseMessage(method.responseID, true, true);
250
+ }
251
+
252
+ const doneAt = preWarmDoneAt.get(key);
253
+ if (typeof doneAt === 'number' && Date.now() - doneAt <= method.preWarmTtl) {
254
+ return createResponseMessage(method.responseID, true, true);
255
+ }
256
+
257
+ const run = onCallDevice(context, message, method);
258
+ preWarmInflight.set(key, run);
259
+ try {
260
+ const result = await run;
261
+ // Only remember the warm if it actually succeeded — a failed pre-warm must
262
+ // not suppress the next pre-warm within the TTL.
263
+ if (result?.success === true && result?.payload === true) {
264
+ preWarmDoneAt.set(key, Date.now());
265
+ }
266
+ return result;
267
+ } finally {
268
+ if (preWarmInflight.get(key) === run) {
269
+ preWarmInflight.delete(key);
270
+ }
271
+ }
272
+ };
273
+
216
274
  const waitWithTimeout = async (promise: Promise<any>, timeout: number) => {
217
275
  const timeoutPromise = new Promise((_, reject) => {
218
276
  setTimeout(() => reject(new Error('Request timeout')), timeout);
@@ -248,7 +306,15 @@ const onCallDevice = async (
248
306
 
249
307
  updateMethodRequestContext(method, { status: 'running' });
250
308
 
251
- const connectStateChange = preConnectCache.passphraseState !== method.payload.passphraseState;
309
+ // Normalize undefined / null / '' to '' — they all mean "main wallet, no
310
+ // passphrase". Without this, the first call (preConnectCache starts undefined)
311
+ // or any '' call after a non-'' one is wrongly treated as a passphrase switch
312
+ // and needlessly clears the device cache -> forces a re-enumeration Initialize.
313
+ // A real switch ('' <-> 'stateX', or 'stateX' <-> 'stateY') still differs.
314
+ const normalizePassphraseState = (s?: string | null) => s || '';
315
+ const connectStateChange =
316
+ normalizePassphraseState(preConnectCache.passphraseState) !==
317
+ normalizePassphraseState(method.payload.passphraseState);
252
318
 
253
319
  preConnectCache = {
254
320
  passphraseState: method.payload.passphraseState,
@@ -268,18 +334,31 @@ const onCallDevice = async (
268
334
 
269
335
  const task = requestQueue.createTask(method);
270
336
 
337
+ // Pre-warm holds the device as a per-connectId callback task so a concurrent
338
+ // real call waits (before ensureConnected) instead of racing its Initialize.
339
+ // Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
340
+ let preWarmCallbackTask: Deferred<void> | undefined;
341
+ if (method.isPreWarmSignal && method.connectId) {
342
+ preWarmCallbackTask = createDeferred<void>();
343
+ context.registerCallbackTask(method.connectId, preWarmCallbackTask);
344
+ }
345
+
271
346
  let device: Device;
272
347
  try {
273
348
  /**
274
349
  * Polling to ensure successful connection
275
350
  */
276
- if (pollingState[pollingId]) {
277
- pollingState[pollingId] = false;
278
- }
279
- pollingId += 1;
280
-
281
- device = await ensureConnected(context, method, pollingId, task.abortController?.signal);
351
+ const connectId = method.connectId ?? '';
352
+ const pollingId = pollingManager.start(connectId);
353
+ device = await ensureConnected(
354
+ context,
355
+ method,
356
+ connectId,
357
+ pollingId,
358
+ task.abortController?.signal
359
+ );
282
360
  } catch (e) {
361
+ preWarmCallbackTask?.resolve();
283
362
  console.log('ensureConnected error: ', e);
284
363
 
285
364
  completeMethodRequestContext(method, e);
@@ -295,6 +374,7 @@ const onCallDevice = async (
295
374
  }
296
375
 
297
376
  if (method.payload?.onlyConnectBleDevice) {
377
+ preWarmCallbackTask?.resolve();
298
378
  Log.debug('Call API - only connect ble device: ', device?.mainId);
299
379
  return createResponseMessage(method.responseID, true, null);
300
380
  }
@@ -334,8 +414,9 @@ const onCallDevice = async (
334
414
  );
335
415
 
336
416
  try {
417
+ // Wait for any pending task except our own (self-wait would deadlock).
337
418
  if (method.connectId) {
338
- await context.waitForCallbackTasks(method.connectId);
419
+ await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
339
420
  }
340
421
 
341
422
  await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
@@ -533,7 +614,6 @@ const onCallDevice = async (
533
614
 
534
615
  try {
535
616
  const response: object = await method.run();
536
- Log.debug('Call API - Inner Method Run: ');
537
617
  messageResponse = createResponseMessage(method.responseID, true, response);
538
618
  requestQueue.resolveRequest(method.responseID, messageResponse);
539
619
  completeMethodRequestContext(method);
@@ -556,6 +636,7 @@ const onCallDevice = async (
556
636
 
557
637
  const runOptions: RunOptions = {
558
638
  keepSession: method.payload.keepSession,
639
+ skipInitialize: canSkipInitialize(method, device),
559
640
  ...parseInitOptions(method),
560
641
  };
561
642
  const deviceRun = () => device.run(inner, runOptions);
@@ -577,6 +658,9 @@ const onCallDevice = async (
577
658
  Log.debug('Call API - Run Error: ', error);
578
659
  completeMethodRequestContext(method, error);
579
660
  } finally {
661
+ // Release the pre-warm callback task so the next real call can proceed.
662
+ preWarmCallbackTask?.resolve();
663
+
580
664
  const response = messageResponse;
581
665
 
582
666
  if (response) {
@@ -707,9 +791,34 @@ function initDeviceForBle(method: BaseMethod) {
707
791
  }
708
792
 
709
793
  /**
710
- * If the Bluetooth connection times out, retry 6 times
794
+ * Check if we can skip initialize for this method
711
795
  */
712
- let bleTimeoutRetry = 0;
796
+ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
797
+ const reasons: string[] = [];
798
+ // only sign-style methods opt in; getAddress/getPublicKey never do
799
+ if (!method.allowUsePreInitialize) reasons.push('method.disallow');
800
+ // caller must opt in per call
801
+ if (!method.payload?.usePreInitialize) reasons.push('payload.usePreInitialize=false');
802
+ // no connectId: can't pin the target device, never skip
803
+ if (!method.connectId) reasons.push('connectId.missing');
804
+ // passphrase state must match the pre-initialize
805
+ if (!device.isPreInitializeMetaMatch(method.payload)) reasons.push('meta.mismatch');
806
+ // device must have been initialized before (has features)
807
+ if (!device.features) reasons.push('features.missing');
808
+ // within pre-initialize TTL
809
+ if (!device.isPreInitializedValid(PRE_INITIALIZE_TTL_MS)) reasons.push('ttl.expired');
810
+
811
+ if (reasons.length) {
812
+ Log.debug(`[PRE-INIT][MISS] method=${method.name} ${reasons.join(',')}`);
813
+ return false;
814
+ }
815
+
816
+ const savedMs = device.getLastInitializeDuration();
817
+ const saved = typeof savedMs === 'number' ? `saved ${savedMs}ms` : 'within TTL + meta match';
818
+ Log.debug(`[PRE-INIT][HIT] method=${method.name} skip Initialize (${saved})`);
819
+
820
+ return true;
821
+ }
713
822
 
714
823
  function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
715
824
  const message = error instanceof Error ? error.message : String(error ?? '');
@@ -721,24 +830,35 @@ function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown)
721
830
  );
722
831
  }
723
832
 
724
- async function connectDeviceForBle(method: BaseMethod, device: Device) {
833
+ /**
834
+ * If the Bluetooth connection times out, retry up to 6 times
835
+ * @param retryCount - Current retry count (default 0)
836
+ */
837
+ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
725
838
  try {
726
839
  await device.acquire(method.payload.connectProtocol);
727
840
  if (method.payload?.onlyConnectBleDevice) {
728
841
  return;
729
842
  }
730
- await device.initialize(parseInitOptions(method));
843
+ // Skip initialize if conditions are met
844
+ if (!canSkipInitialize(method, device)) {
845
+ const initOptions = parseInitOptions(method);
846
+ await device.initialize(initOptions);
847
+ device.markPreInitialized({
848
+ passphraseState: initOptions.passphraseState,
849
+ });
850
+ }
731
851
  } catch (err) {
732
852
  if (
733
853
  (err.errorCode === HardwareErrorCode.BleTimeoutError ||
734
854
  err.errorCode === HardwareErrorCode.BleConnectedError ||
735
855
  isRetryableBleProtocolV2ProbeError(method, err)) &&
736
- bleTimeoutRetry <= 5
856
+ retryCount < 6
737
857
  ) {
738
- bleTimeoutRetry += 1;
739
- Log.debug(`Bletooth connect timeout and will retry, retry count: ${bleTimeoutRetry}`);
858
+ const nextRetry = retryCount + 1;
859
+ Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
740
860
  await wait(3000);
741
- await connectDeviceForBle(method, device);
861
+ await connectDeviceForBle(method, device, nextRetry);
742
862
  } else {
743
863
  throw err;
744
864
  }
@@ -750,6 +870,7 @@ type IPollFn<T> = (time?: number) => T;
750
870
  const ensureConnected = async (
751
871
  _context: CoreContext,
752
872
  method: BaseMethod,
873
+ connectId: string,
753
874
  pollingId: number,
754
875
  abortSignal?: AbortSignal
755
876
  ) => {
@@ -781,7 +902,7 @@ const ensureConnected = async (
781
902
  return;
782
903
  }
783
904
 
784
- if (!pollingState[pollingId]) {
905
+ if (!pollingManager.isActive(connectId, pollingId)) {
785
906
  Log.debug('EnsureConnected function stop, polling id: ', pollingId);
786
907
  reject(ERRORS.TypedError(HardwareErrorCode.PollingStop));
787
908
  return;
@@ -839,8 +960,6 @@ const ensureConnected = async (
839
960
  * Bluetooth should call initialize here
840
961
  */
841
962
  if (DataManager.isBleConnect(env)) {
842
- bleTimeoutRetry = 0;
843
-
844
963
  if (abort()) {
845
964
  return;
846
965
  }
@@ -903,7 +1022,7 @@ const ensureConnected = async (
903
1022
  // eslint-disable-next-line no-promise-executor-return
904
1023
  return setTimeout(() => resolve(poll(time * 1.5)), time);
905
1024
  });
906
- pollingState[pollingId] = true;
1025
+ // pollingManager.start(connectId) already registered this pollingId as active
907
1026
  return poll();
908
1027
  };
909
1028
 
@@ -1015,11 +1134,7 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) =>
1015
1134
  const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
1016
1135
  if (!method.useDevicePassphraseState) return false;
1017
1136
 
1018
- const isPro2 = getDeviceType(device.features) === EDeviceType.Pro2;
1019
- const pro2ExplicitWalletSelection =
1020
- isPro2 && (!!method.payload?.passphraseState || !!method.payload?.useEmptyPassphrase);
1021
-
1022
- return device.hasUsePassphrase() || pro2ExplicitWalletSelection;
1137
+ return device.hasUsePassphrase();
1023
1138
  };
1024
1139
 
1025
1140
  const cleanup = () => {
@@ -1047,6 +1162,7 @@ const onDeviceConnectHandler = (device: Device) => {
1047
1162
  };
1048
1163
 
1049
1164
  const onDeviceDisconnectHandler = (device: Device) => {
1165
+ device.clearPreInitialized();
1050
1166
  const env = DataManager.getSettings('env');
1051
1167
  const deviceObject = DataManager.isBleConnect(env) ? device : device.toMessageObject();
1052
1168
  postMessage(createDeviceMessage(DEVICE.DISCONNECT, { device: deviceObject as KnownDevice }));
@@ -1223,8 +1339,8 @@ export default class Core extends EventEmitter {
1223
1339
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
1224
1340
  this.requestQueue.registerPendingCallbackTask(connectId, callbackPromise);
1225
1341
  },
1226
- waitForCallbackTasks: (connectId: string) =>
1227
- this.requestQueue.waitForPendingCallbackTasks(connectId),
1342
+ waitForCallbackTasks: (connectId: string, exceptTask?: Deferred<void>) =>
1343
+ this.requestQueue.waitForPendingCallbackTasks(connectId, exceptTask),
1228
1344
  cancelCallbackTasks: (connectId: string) => this.requestQueue.cancelCallbackTasks(connectId),
1229
1345
  };
1230
1346
  }
@@ -1255,10 +1371,10 @@ export default class Core extends EventEmitter {
1255
1371
  }
1256
1372
 
1257
1373
  case IFRAME.CALL: {
1258
- Log.log('call API: ', message);
1374
+ Log.log(`[${Date.now()}][CALL_API]`, message);
1259
1375
  const response = await callAPI(this.getCoreContext(), message);
1260
1376
  const { success, payload } = response;
1261
- Log.log('call API Response: ', response);
1377
+ Log.log(`[${Date.now()}][CALL_API_RESPONSE]`, response);
1262
1378
  if (success) {
1263
1379
  return response;
1264
1380
  }
@@ -1291,6 +1407,9 @@ export default class Core extends EventEmitter {
1291
1407
  dispose() {
1292
1408
  _deviceList = undefined;
1293
1409
  _connector = undefined;
1410
+ deviceCacheMap.clear();
1411
+ preWarmInflight.clear();
1412
+ preWarmDoneAt.clear();
1294
1413
  Log.debug(`[Core] Disposing SDK instance: ${this.sdkInstanceId}`);
1295
1414
  cleanupSdkInstance(this.sdkInstanceId);
1296
1415
  }
@@ -64,6 +64,7 @@ export type InitOptions = {
64
64
 
65
65
  export type RunOptions = {
66
66
  keepSession?: boolean;
67
+ skipInitialize?: boolean;
67
68
  } & InitOptions;
68
69
 
69
70
  const parseRunOptions = (options?: RunOptions): RunOptions => {
@@ -198,6 +199,17 @@ export class Device extends EventEmitter {
198
199
 
199
200
  pendingCallbackPromise?: Deferred<void>;
200
201
 
202
+ /** Pre-initialize timestamp (ms) */
203
+ private preInitializedAt?: number;
204
+
205
+ /** Pre-initialize context, used to verify state consistency before skipping */
206
+ private preInitializeMeta?: {
207
+ passphraseState?: string;
208
+ };
209
+
210
+ /** Last Initialize duration (ms), reported as "saved" when a skip happens */
211
+ private lastInitializeDurationMs?: number;
212
+
201
213
  constructor(descriptor: DeviceDescriptor, sdkInstanceId?: string) {
202
214
  super();
203
215
  this.originalDescriptor = descriptor;
@@ -378,6 +390,54 @@ export class Device extends EventEmitter {
378
390
  this.deviceAcquired = false;
379
391
  }
380
392
 
393
+ /**
394
+ * Pre-initialize: connect + Initialize ahead of the sign. Only runs the
395
+ * fallback init when features are missing (gate on `!this.features`, not
396
+ * isUsedHere which is always false on BLE); otherwise just records the mark.
397
+ */
398
+ async preInitialize(initOptions?: InitOptions) {
399
+ if (!this.features) {
400
+ await this.acquire();
401
+ await this.initialize(initOptions);
402
+ }
403
+ this.markPreInitialized({
404
+ passphraseState: initOptions?.passphraseState,
405
+ });
406
+ }
407
+
408
+ markPreInitialized(meta?: { passphraseState?: string }) {
409
+ this.preInitializedAt = Date.now();
410
+ this.preInitializeMeta = meta
411
+ ? {
412
+ passphraseState: meta.passphraseState === '' ? undefined : meta.passphraseState,
413
+ }
414
+ : undefined;
415
+ }
416
+
417
+ clearPreInitialized() {
418
+ this.preInitializedAt = undefined;
419
+ this.preInitializeMeta = undefined;
420
+ }
421
+
422
+ isPreInitializeMetaMatch(payload?: { passphraseState?: string }) {
423
+ if (!this.preInitializeMeta) return true;
424
+ const passphraseState = payload?.passphraseState === '' ? undefined : payload?.passphraseState;
425
+ return this.preInitializeMeta.passphraseState === passphraseState;
426
+ }
427
+
428
+ isPreInitializedValid(ttlMs: number) {
429
+ if (!this.preInitializedAt) return false;
430
+ return Date.now() - this.preInitializedAt <= ttlMs;
431
+ }
432
+
433
+ setLastInitializeDuration(durationMs: number) {
434
+ this.lastInitializeDurationMs = durationMs;
435
+ }
436
+
437
+ getLastInitializeDuration() {
438
+ return this.lastInitializeDurationMs;
439
+ }
440
+
381
441
  getCommands() {
382
442
  return this.commands;
383
443
  }
@@ -516,13 +576,7 @@ export class Device extends EventEmitter {
516
576
  payload.passphrase_state = options?.passphraseState;
517
577
  payload.is_contains_attach = true;
518
578
 
519
- Log.debug('Initialize device begin:', {
520
- deviceId: options?.deviceId,
521
- passphraseState: options?.passphraseState,
522
- initSession: options?.initSession,
523
- InitializePayload: payload,
524
- });
525
-
579
+ const initStartAt = Date.now();
526
580
  try {
527
581
  // @ts-expect-error
528
582
  const { message } = await Promise.race([
@@ -535,7 +589,8 @@ export class Device extends EventEmitter {
535
589
  }),
536
590
  ]);
537
591
 
538
- Log.debug('Initialize device end: ', message);
592
+ const initCostMs = Date.now() - initStartAt;
593
+ this.setLastInitializeDuration(initCostMs);
539
594
  this._updateFeatures(message, options?.initSession);
540
595
  await TransportManager.reconfigure(this.features);
541
596
  } catch (error) {
@@ -555,11 +610,7 @@ export class Device extends EventEmitter {
555
610
 
556
611
  try {
557
612
  const features = await Promise.race([
558
- getProtocolV2Features({
559
- commands: this.commands,
560
- descriptor: this.originalDescriptor,
561
- timeoutMs: 10 * 1000,
562
- }),
613
+ this._readProtocolV2Features(10 * 1000),
563
614
  new Promise<never>((_, reject) => {
564
615
  setTimeout(() => {
565
616
  reject(ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed));
@@ -574,9 +625,24 @@ export class Device extends EventEmitter {
574
625
  }
575
626
  }
576
627
 
628
+ private async _readProtocolV2Features(timeoutMs?: number) {
629
+ return getProtocolV2Features({
630
+ commands: this.commands,
631
+ descriptor: this.originalDescriptor,
632
+ timeoutMs,
633
+ });
634
+ }
635
+
577
636
  async getFeatures() {
637
+ if (this.originalDescriptor.protocolType === 'V2') {
638
+ const features = await this._readProtocolV2Features();
639
+ this._updateFeatures(features);
640
+ return features;
641
+ }
642
+
578
643
  const { message } = await this.commands.typedCall('GetFeatures', 'Features', {});
579
644
  this._updateFeatures(message);
645
+ return message;
580
646
  }
581
647
 
582
648
  _updateFeatures(feat: Features, initSession?: boolean) {
@@ -652,7 +718,9 @@ export class Device extends EventEmitter {
652
718
 
653
719
  try {
654
720
  if (fn) {
655
- await this.initialize(options);
721
+ if (!options?.skipInitialize) {
722
+ await this.initialize(options);
723
+ }
656
724
  }
657
725
  } catch (error) {
658
726
  this.runPromise = null;
@@ -849,7 +917,8 @@ export class Device extends EventEmitter {
849
917
  hasUsePassphrase() {
850
918
  const isModeT =
851
919
  getDeviceType(this.features) === EDeviceType.Touch ||
852
- getDeviceType(this.features) === EDeviceType.Pro;
920
+ getDeviceType(this.features) === EDeviceType.Pro ||
921
+ getDeviceType(this.features) === EDeviceType.Pro2;
853
922
  const preCheckTouch = isModeT && this.features?.unlocked === false;
854
923
 
855
924
  return this.features && (!!this.features.passphrase_protection || preCheckTouch);
@@ -863,10 +932,6 @@ export class Device extends EventEmitter {
863
932
  }
864
933
 
865
934
  async lockDevice(): Promise<Success> {
866
- if (getDeviceType(this.features) === EDeviceType.Pro2) {
867
- return { message: 'LockDevice skipped for Pro2' };
868
- }
869
-
870
935
  const res = await this.commands.typedCall('LockDevice', 'Success', {});
871
936
  return res.message;
872
937
  }
@@ -876,6 +941,9 @@ export class Device extends EventEmitter {
876
941
  pro: {
877
942
  min: '4.15.0',
878
943
  },
944
+ pro2: {
945
+ min: '4.15.0',
946
+ },
879
947
  };
880
948
  }
881
949
 
@@ -890,12 +958,8 @@ export class Device extends EventEmitter {
890
958
  this.features,
891
959
  Enum_Capability.Capability_AttachToPin
892
960
  );
893
- const isPro2 = getDeviceType(this.features) === EDeviceType.Pro2;
894
-
895
961
  const supportUnlock =
896
962
  supportAttachPinCapability ||
897
- // Pro2 V2 暂未从 features 暴露 capabilities,先直连该方法用于固件联调。
898
- isPro2 ||
899
963
  (versionRange && semver.gte(firmwareVersion, versionRange.min));
900
964
 
901
965
  if (supportUnlock) {
@@ -910,9 +974,8 @@ export class Device extends EventEmitter {
910
974
  return Promise.resolve(this.features);
911
975
  }
912
976
 
913
- const featuresRes = await this.commands.typedCall('GetFeatures', 'Features');
914
- this._updateFeatures(featuresRes.message);
915
- return Promise.resolve(featuresRes.message);
977
+ const features = await this.getFeatures();
978
+ return Promise.resolve(features);
916
979
  }
917
980
 
918
981
  const { type } = await this.commands.typedCall('GetAddress', 'Address', {
@@ -926,9 +989,8 @@ export class Device extends EventEmitter {
926
989
  if (type === 'CallMethodError') {
927
990
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'unlock device error');
928
991
  }
929
- const res = await this.commands.typedCall('GetFeatures', 'Features');
930
- this._updateFeatures(res.message);
931
- return Promise.resolve(res.message);
992
+ const features = await this.getFeatures();
993
+ return Promise.resolve(features);
932
994
  }
933
995
 
934
996
  async checkPassphraseStateSafety(
package/src/inject.ts CHANGED
@@ -143,7 +143,7 @@ export const createCoreApi = (
143
143
 
144
144
  testInitializeDeviceDuration: (connectId, params) =>
145
145
  call({ ...params, connectId, method: 'testInitializeDeviceDuration' }),
146
-
146
+ preInitialize: (connectId, params) => call({ ...params, connectId, method: 'preInitialize' }),
147
147
  deviceBackup: connectId => call({ connectId, method: 'deviceBackup' }),
148
148
  deviceChangePin: (connectId, params) => call({ ...params, connectId, method: 'deviceChangePin' }),
149
149
  deviceFlags: (connectId, params) => call({ ...params, connectId, method: 'deviceFlags' }),
@@ -144,7 +144,7 @@ function createBaseFeatures(descriptor: DeviceDescriptor): Features {
144
144
  minor_version: 0,
145
145
  patch_version: 0,
146
146
  bootloader_mode: false,
147
- device_id: descriptorId,
147
+ device_id: '',
148
148
  pin_protection: null,
149
149
  passphrase_protection: null,
150
150
  language: null,
@@ -190,8 +190,7 @@ export function normalizeProtocolV2Features(
190
190
  const features = createBaseFeatures(descriptor);
191
191
  if (!deviceInfo) return features;
192
192
 
193
- const serialNo =
194
- deviceInfo.hw?.serial_no || features.onekey_serial_no || getDescriptorId(descriptor);
193
+ const serialNo = deviceInfo.hw?.serial_no;
195
194
  const firmwareVersion = getImageVersion(deviceInfo.fw?.app);
196
195
  const [fwMajor, fwMinor, fwPatch] = parseVersion(firmwareVersion);
197
196
 
@@ -203,9 +202,9 @@ export function normalizeProtocolV2Features(
203
202
  fw_major: fwMajor,
204
203
  fw_minor: fwMinor,
205
204
  fw_patch: fwPatch,
206
- device_id: serialNo,
207
- serial_no: serialNo,
208
- onekey_serial_no: serialNo,
205
+ device_id: serialNo ?? features.device_id,
206
+ serial_no: serialNo ?? features.serial_no,
207
+ onekey_serial_no: serialNo ?? features.onekey_serial_no,
209
208
  protocol_version: deviceInfo.protocol_version ?? features.protocol_version,
210
209
  label: deviceInfo.status?.label ?? features.label,
211
210
  language: deviceInfo.status?.language ?? features.language,
@@ -30,6 +30,7 @@ import type { off, on, removeAllListeners } from './event';
30
30
  import type { uiResponse } from './uiResponse';
31
31
  import type { init, updateSettings } from './init';
32
32
  import type { testInitializeDeviceDuration } from './testInitializeDeviceDuration';
33
+ import type { preInitialize } from './preInitialize';
33
34
  import type { getLogs } from './getLogs';
34
35
  import type { checkBridgeStatus } from './checkBridgeStatus';
35
36
  import type { checkBridgeRelease } from './checkBridgeRelease';
@@ -199,6 +200,7 @@ export type CoreApi = {
199
200
  * Test function
200
201
  */
201
202
  testInitializeDeviceDuration: typeof testInitializeDeviceDuration;
203
+ preInitialize: typeof preInitialize;
202
204
 
203
205
  /**
204
206
  * Core function
@@ -0,0 +1,3 @@
1
+ import type { CommonParams, Response } from '../params';
2
+
3
+ export declare function preInitialize(connectId: string, params?: CommonParams): Response<boolean>;
@@ -50,6 +50,11 @@ export interface CommonParams {
50
50
  */
51
51
  onlyConnectBleDevice?: boolean;
52
52
 
53
+ /**
54
+ * Use pre-initialized device state (BLE only)
55
+ */
56
+ usePreInitialize?: boolean;
57
+
53
58
  /**
54
59
  * Expected transport protocol. If omitted, SDK probes Protocol V1 then Protocol V2.
55
60
  */