@onekeyfe/hd-core 1.1.26-patch.2 → 1.1.27-alpha.1

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 (129) hide show
  1. package/dist/api/BaseMethod.d.ts +4 -0
  2. package/dist/api/BaseMethod.d.ts.map +1 -1
  3. package/dist/api/alephium/AlephiumSignMessage.d.ts.map +1 -1
  4. package/dist/api/alephium/AlephiumSignTransaction.d.ts.map +1 -1
  5. package/dist/api/algo/AlgoSignTransaction.d.ts.map +1 -1
  6. package/dist/api/aptos/AptosSignInMessage.d.ts.map +1 -1
  7. package/dist/api/aptos/AptosSignMessage.d.ts.map +1 -1
  8. package/dist/api/aptos/AptosSignTransaction.d.ts.map +1 -1
  9. package/dist/api/benfen/BenfenSignMessage.d.ts.map +1 -1
  10. package/dist/api/benfen/BenfenSignTransaction.d.ts.map +1 -1
  11. package/dist/api/btc/BTCSignMessage.d.ts.map +1 -1
  12. package/dist/api/btc/BTCSignPsbt.d.ts.map +1 -1
  13. package/dist/api/btc/BTCSignTransaction.d.ts.map +1 -1
  14. package/dist/api/cardano/CardanoSignMessage.d.ts.map +1 -1
  15. package/dist/api/cardano/CardanoSignTransaction.d.ts.map +1 -1
  16. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  17. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  18. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  19. package/dist/api/cosmos/CosmosSignTransaction.d.ts.map +1 -1
  20. package/dist/api/device/PreInitialize.d.ts +6 -0
  21. package/dist/api/device/PreInitialize.d.ts.map +1 -0
  22. package/dist/api/dynex/DnxSignTransaction.d.ts.map +1 -1
  23. package/dist/api/evm/EVMSignMessage.d.ts.map +1 -1
  24. package/dist/api/evm/EVMSignMessageEIP712.d.ts.map +1 -1
  25. package/dist/api/evm/EVMSignTransaction.d.ts.map +1 -1
  26. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  27. package/dist/api/filecoin/FilecoinSignTransaction.d.ts.map +1 -1
  28. package/dist/api/index.d.ts +1 -0
  29. package/dist/api/index.d.ts.map +1 -1
  30. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  31. package/dist/api/near/NearSignTransaction.d.ts.map +1 -1
  32. package/dist/api/nem/NEMSignTransaction.d.ts.map +1 -1
  33. package/dist/api/neo/NeoSignTransaction.d.ts.map +1 -1
  34. package/dist/api/nervos/NervosSignTransaction.d.ts.map +1 -1
  35. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  36. package/dist/api/nostr/NostrSignEvent.d.ts.map +1 -1
  37. package/dist/api/nostr/NostrSignSchnorr.d.ts.map +1 -1
  38. package/dist/api/polkadot/PolkadotSignTransaction.d.ts.map +1 -1
  39. package/dist/api/scdo/ScdoSignMessage.d.ts.map +1 -1
  40. package/dist/api/scdo/ScdoSignTransaction.d.ts.map +1 -1
  41. package/dist/api/solana/SolSignMessage.d.ts.map +1 -1
  42. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  43. package/dist/api/solana/SolSignTransaction.d.ts.map +1 -1
  44. package/dist/api/starcoin/StarcoinSignMessage.d.ts.map +1 -1
  45. package/dist/api/starcoin/StarcoinSignTransaction.d.ts.map +1 -1
  46. package/dist/api/stellar/StellarSignTransaction.d.ts.map +1 -1
  47. package/dist/api/sui/SuiSignMessage.d.ts.map +1 -1
  48. package/dist/api/sui/SuiSignTransaction.d.ts.map +1 -1
  49. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  50. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  51. package/dist/api/ton/TonSignProof.d.ts.map +1 -1
  52. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  53. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  54. package/dist/api/xrp/XrpSignTransaction.d.ts.map +1 -1
  55. package/dist/core/PollingStateManager.d.ts +8 -0
  56. package/dist/core/PollingStateManager.d.ts.map +1 -0
  57. package/dist/core/RequestQueue.d.ts +1 -1
  58. package/dist/core/RequestQueue.d.ts.map +1 -1
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/device/Device.d.ts +17 -0
  61. package/dist/device/Device.d.ts.map +1 -1
  62. package/dist/index.d.ts +21 -0
  63. package/dist/index.js +347 -113
  64. package/dist/types/api/index.d.ts +2 -0
  65. package/dist/types/api/index.d.ts.map +1 -1
  66. package/dist/types/api/preInitialize.d.ts +3 -0
  67. package/dist/types/api/preInitialize.d.ts.map +1 -0
  68. package/dist/types/params.d.ts +1 -0
  69. package/dist/types/params.d.ts.map +1 -1
  70. package/package.json +4 -4
  71. package/src/api/BaseMethod.ts +24 -0
  72. package/src/api/alephium/AlephiumSignMessage.ts +1 -0
  73. package/src/api/alephium/AlephiumSignTransaction.ts +1 -0
  74. package/src/api/algo/AlgoSignTransaction.ts +1 -0
  75. package/src/api/aptos/AptosSignInMessage.ts +1 -0
  76. package/src/api/aptos/AptosSignMessage.ts +1 -0
  77. package/src/api/aptos/AptosSignTransaction.ts +1 -0
  78. package/src/api/benfen/BenfenSignMessage.ts +1 -0
  79. package/src/api/benfen/BenfenSignTransaction.ts +1 -0
  80. package/src/api/btc/BTCSignMessage.ts +1 -0
  81. package/src/api/btc/BTCSignPsbt.ts +1 -0
  82. package/src/api/btc/BTCSignTransaction.ts +1 -0
  83. package/src/api/cardano/CardanoSignMessage.ts +1 -0
  84. package/src/api/cardano/CardanoSignTransaction.ts +1 -0
  85. package/src/api/conflux/ConfluxSignMessage.ts +1 -0
  86. package/src/api/conflux/ConfluxSignMessageCIP23.ts +1 -0
  87. package/src/api/conflux/ConfluxSignTransaction.ts +1 -0
  88. package/src/api/cosmos/CosmosSignTransaction.ts +1 -0
  89. package/src/api/device/PreInitialize.ts +43 -0
  90. package/src/api/dynex/DnxSignTransaction.ts +1 -0
  91. package/src/api/evm/EVMSignMessage.ts +2 -0
  92. package/src/api/evm/EVMSignMessageEIP712.ts +1 -0
  93. package/src/api/evm/EVMSignTransaction.ts +2 -0
  94. package/src/api/evm/EVMSignTypedData.ts +2 -0
  95. package/src/api/filecoin/FilecoinSignTransaction.ts +1 -0
  96. package/src/api/index.ts +1 -0
  97. package/src/api/kaspa/KaspaSignTransaction.ts +1 -0
  98. package/src/api/near/NearSignTransaction.ts +1 -0
  99. package/src/api/nem/NEMSignTransaction.ts +1 -0
  100. package/src/api/neo/NeoSignTransaction.ts +1 -0
  101. package/src/api/nervos/NervosSignTransaction.ts +1 -0
  102. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  103. package/src/api/nostr/NostrSignEvent.ts +1 -0
  104. package/src/api/nostr/NostrSignSchnorr.ts +1 -0
  105. package/src/api/polkadot/PolkadotSignTransaction.ts +1 -0
  106. package/src/api/scdo/ScdoSignMessage.ts +1 -0
  107. package/src/api/scdo/ScdoSignTransaction.ts +1 -0
  108. package/src/api/solana/SolSignMessage.ts +1 -0
  109. package/src/api/solana/SolSignOffchainMessage.ts +1 -0
  110. package/src/api/solana/SolSignTransaction.ts +1 -0
  111. package/src/api/starcoin/StarcoinSignMessage.ts +1 -0
  112. package/src/api/starcoin/StarcoinSignTransaction.ts +1 -0
  113. package/src/api/stellar/StellarSignTransaction.ts +1 -0
  114. package/src/api/sui/SuiSignMessage.ts +1 -0
  115. package/src/api/sui/SuiSignTransaction.ts +1 -0
  116. package/src/api/ton/TonSignData.ts +1 -0
  117. package/src/api/ton/TonSignMessage.ts +1 -0
  118. package/src/api/ton/TonSignProof.ts +1 -0
  119. package/src/api/tron/TronSignMessage.ts +1 -0
  120. package/src/api/tron/TronSignTransaction.ts +1 -0
  121. package/src/api/xrp/XrpSignTransaction.ts +1 -0
  122. package/src/core/PollingStateManager.ts +47 -0
  123. package/src/core/RequestQueue.ts +10 -3
  124. package/src/core/index.ts +145 -27
  125. package/src/device/Device.ts +73 -9
  126. package/src/inject.ts +1 -1
  127. package/src/types/api/index.ts +2 -0
  128. package/src/types/api/preInitialize.ts +3 -0
  129. package/src/types/params.ts +5 -0
@@ -26,6 +26,7 @@ export default class TonSignMessage extends BaseMethod<HardwareTonSignMessage> {
26
26
  this.strictCheckDeviceSupport = true;
27
27
  this.checkDeviceId = true;
28
28
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
29
+ this.allowUsePreInitialize = true;
29
30
 
30
31
  // init params
31
32
  validateParams(this.payload, [
@@ -11,6 +11,7 @@ export default class TonSignProof extends BaseMethod<HardwareTonSignProof> {
11
11
  this.strictCheckDeviceSupport = true;
12
12
  this.checkDeviceId = true;
13
13
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
14
+ this.allowUsePreInitialize = true;
14
15
 
15
16
  // init params
16
17
  validateParams(this.payload, [
@@ -14,6 +14,7 @@ export default class TronSignMessage extends BaseMethod<HardwareTronSignMessage>
14
14
  init() {
15
15
  this.checkDeviceId = true;
16
16
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
17
+ this.allowUsePreInitialize = true;
17
18
 
18
19
  // check payload
19
20
  validateParams(this.payload, [
@@ -123,6 +123,7 @@ export default class TronSignTransaction extends BaseMethod<TronSignTx> {
123
123
  init() {
124
124
  this.checkDeviceId = true;
125
125
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
126
+ this.allowUsePreInitialize = true;
126
127
 
127
128
  // check payload
128
129
  validateParams(this.payload, [
@@ -11,6 +11,7 @@ export default class XrpGetAddress extends BaseMethod<XrpSignTransactionParams>
11
11
  init() {
12
12
  this.checkDeviceId = true;
13
13
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
14
+ this.allowUsePreInitialize = true;
14
15
 
15
16
  const { payload } = this;
16
17
  validateParams(payload, [
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Manages polling state for device connection attempts.
3
+ *
4
+ * Polling is isolated by connectId (device), so:
5
+ * - New request for device A only stops device A's previous polling
6
+ * - Device B's polling is unaffected
7
+ */
8
+ export class PollingStateManager {
9
+ // connectId -> current polling ID
10
+ private activePolls = new Map<string, number>();
11
+
12
+ /**
13
+ * Start a new polling session for a device.
14
+ * Automatically stops the previous polling for the same device.
15
+ * @param connectId - Device identifier (use empty string for USB without connectId)
16
+ * @returns The new polling ID
17
+ */
18
+ start(connectId: string): number {
19
+ const currentId = (this.activePolls.get(connectId) ?? 0) + 1;
20
+ this.activePolls.set(connectId, currentId);
21
+ return currentId;
22
+ }
23
+
24
+ /**
25
+ * Check if a polling session is still active.
26
+ * @param connectId - Device identifier
27
+ * @param pollingId - The polling ID to check
28
+ */
29
+ isActive(connectId: string, pollingId: number): boolean {
30
+ return this.activePolls.get(connectId) === pollingId;
31
+ }
32
+
33
+ /**
34
+ * Stop polling for a specific device.
35
+ * @param connectId - Device identifier
36
+ */
37
+ stop(connectId: string): void {
38
+ this.activePolls.delete(connectId);
39
+ }
40
+
41
+ /**
42
+ * Stop all active polling sessions.
43
+ */
44
+ stopAll(): void {
45
+ this.activePolls.clear();
46
+ }
47
+ }
@@ -113,13 +113,20 @@ export default class RequestQueue {
113
113
 
114
114
  callbackPromise.promise.finally(() => {
115
115
  Log.debug(`Callback task completed for connectId: ${connectId}`);
116
- this.pendingCallbackTasks.delete(connectId);
116
+ // Delete by identity so a newer task that replaced this slot isn't orphaned.
117
+ if (this.pendingCallbackTasks.get(connectId) === callbackPromise) {
118
+ this.pendingCallbackTasks.delete(connectId);
119
+ }
117
120
  });
118
121
  }
119
122
 
120
- public async waitForPendingCallbackTasks(connectId: string): Promise<void> {
123
+ public async waitForPendingCallbackTasks(
124
+ connectId: string,
125
+ exceptTask?: Deferred<void>
126
+ ): Promise<void> {
121
127
  const pendingTask = this.pendingCallbackTasks.get(connectId);
122
- if (pendingTask) {
128
+ // Skip only the caller's own task (self-wait); a different one is still awaited.
129
+ if (pendingTask && pendingTask !== exceptTask) {
123
130
  Log.debug(`Waiting for pending callback task to complete for connectId: ${connectId}`);
124
131
  await pendingTask.promise;
125
132
  }
package/src/core/index.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  import { Device } from '../device/Device';
43
43
  import { DeviceList } from '../device/DeviceList';
44
44
  import { DevicePool } from '../device/DevicePool';
45
+ import { PollingStateManager } from './PollingStateManager';
45
46
  import { findMethod } from '../api/utils';
46
47
  import { DataManager } from '../data-manager';
47
48
  import { UI_REQUEST as UI_REQUEST_CONST } from '../constants/ui-request';
@@ -73,6 +74,12 @@ import type {
73
74
  import type { BaseMethod } from '../api/BaseMethod';
74
75
 
75
76
  const Log = getLogger(LoggerNames.Core);
77
+ const PRE_INITIALIZE_TTL_MS = 60 * 1000;
78
+
79
+ // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
80
+ // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
81
+ const preWarmInflight = new Map<string, Promise<any>>();
82
+ const preWarmDoneAt = new Map<string, number>();
76
83
 
77
84
  export type CoreContext = ReturnType<Core['getCoreContext']>;
78
85
 
@@ -103,8 +110,7 @@ let _connector: DeviceConnector | undefined;
103
110
  let _uiPromises: UiPromise<UiPromiseResponse['type']>[] = []; // Waiting for ui response
104
111
 
105
112
  const deviceCacheMap = new Map<string, Device>();
106
- let pollingId = 1;
107
- const pollingState: Record<number, boolean> = {};
113
+ const pollingManager = new PollingStateManager();
108
114
 
109
115
  let preConnectCache: {
110
116
  passphraseState: string | undefined;
@@ -206,9 +212,56 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
206
212
  return createResponseMessage(method.responseID, false, { error });
207
213
  }
208
214
 
215
+ if (method.isPreWarmSignal) {
216
+ return handlePreWarmSignal(context, message, method);
217
+ }
218
+
209
219
  return onCallDevice(context, message, method);
210
220
  };
211
221
 
222
+ // Wrapper for "pre-warm signal" methods: coalesce in-flight same-key pre-warm,
223
+ // skip if warmed within TTL, else run + track. The "hang up so the next real
224
+ // call waits" part lives in onCallDevice (setPrePendingCallPromise).
225
+ const handlePreWarmSignal = async (
226
+ context: CoreContext,
227
+ message: CoreMessage,
228
+ method: BaseMethod
229
+ ): Promise<any> => {
230
+ const key = method.getPreWarmKey();
231
+
232
+ const inflight = preWarmInflight.get(key);
233
+ if (inflight) {
234
+ // reply with THIS call's responseID (not the other call's response object)
235
+ try {
236
+ await inflight;
237
+ } catch {
238
+ // pre-warm is best-effort; ignore its failure for the coalesced caller
239
+ }
240
+ return createResponseMessage(method.responseID, true, true);
241
+ }
242
+
243
+ const doneAt = preWarmDoneAt.get(key);
244
+ if (typeof doneAt === 'number' && Date.now() - doneAt <= method.preWarmTtl) {
245
+ return createResponseMessage(method.responseID, true, true);
246
+ }
247
+
248
+ const run = onCallDevice(context, message, method);
249
+ preWarmInflight.set(key, run);
250
+ try {
251
+ const result = await run;
252
+ // Only remember the warm if it actually succeeded — a failed pre-warm must
253
+ // not suppress the next pre-warm within the TTL.
254
+ if (result?.success === true && result?.payload === true) {
255
+ preWarmDoneAt.set(key, Date.now());
256
+ }
257
+ return result;
258
+ } finally {
259
+ if (preWarmInflight.get(key) === run) {
260
+ preWarmInflight.delete(key);
261
+ }
262
+ }
263
+ };
264
+
212
265
  const waitWithTimeout = async (promise: Promise<any>, timeout: number) => {
213
266
  const timeoutPromise = new Promise((_, reject) => {
214
267
  setTimeout(() => reject(new Error('Request timeout')), timeout);
@@ -244,7 +297,15 @@ const onCallDevice = async (
244
297
 
245
298
  updateMethodRequestContext(method, { status: 'running' });
246
299
 
247
- const connectStateChange = preConnectCache.passphraseState !== method.payload.passphraseState;
300
+ // Normalize undefined / null / '' to '' — they all mean "main wallet, no
301
+ // passphrase". Without this, the first call (preConnectCache starts undefined)
302
+ // or any '' call after a non-'' one is wrongly treated as a passphrase switch
303
+ // and needlessly clears the device cache -> forces a re-enumeration Initialize.
304
+ // A real switch ('' <-> 'stateX', or 'stateX' <-> 'stateY') still differs.
305
+ const normalizePassphraseState = (s?: string | null) => s || '';
306
+ const connectStateChange =
307
+ normalizePassphraseState(preConnectCache.passphraseState) !==
308
+ normalizePassphraseState(method.payload.passphraseState);
248
309
 
249
310
  preConnectCache = {
250
311
  passphraseState: method.payload.passphraseState,
@@ -264,18 +325,31 @@ const onCallDevice = async (
264
325
 
265
326
  const task = requestQueue.createTask(method);
266
327
 
328
+ // Pre-warm holds the device as a per-connectId callback task so a concurrent
329
+ // real call waits (before ensureConnected) instead of racing its Initialize.
330
+ // Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
331
+ let preWarmCallbackTask: Deferred<void> | undefined;
332
+ if (method.isPreWarmSignal && method.connectId) {
333
+ preWarmCallbackTask = createDeferred<void>();
334
+ context.registerCallbackTask(method.connectId, preWarmCallbackTask);
335
+ }
336
+
267
337
  let device: Device;
268
338
  try {
269
339
  /**
270
340
  * Polling to ensure successful connection
271
341
  */
272
- if (pollingState[pollingId]) {
273
- pollingState[pollingId] = false;
274
- }
275
- pollingId += 1;
276
-
277
- device = await ensureConnected(context, method, pollingId, task.abortController?.signal);
342
+ const connectId = method.connectId ?? '';
343
+ const pollingId = pollingManager.start(connectId);
344
+ device = await ensureConnected(
345
+ context,
346
+ method,
347
+ connectId,
348
+ pollingId,
349
+ task.abortController?.signal
350
+ );
278
351
  } catch (e) {
352
+ preWarmCallbackTask?.resolve();
279
353
  console.log('ensureConnected error: ', e);
280
354
 
281
355
  completeMethodRequestContext(method, e);
@@ -291,6 +365,7 @@ const onCallDevice = async (
291
365
  }
292
366
 
293
367
  if (method.payload?.onlyConnectBleDevice) {
368
+ preWarmCallbackTask?.resolve();
294
369
  Log.debug('Call API - only connect ble device: ', device?.mainId);
295
370
  return createResponseMessage(method.responseID, true, null);
296
371
  }
@@ -330,8 +405,9 @@ const onCallDevice = async (
330
405
  );
331
406
 
332
407
  try {
408
+ // Wait for any pending task except our own (self-wait would deadlock).
333
409
  if (method.connectId) {
334
- await context.waitForCallbackTasks(method.connectId);
410
+ await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
335
411
  }
336
412
 
337
413
  await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
@@ -525,7 +601,6 @@ const onCallDevice = async (
525
601
 
526
602
  try {
527
603
  const response: object = await method.run();
528
- Log.debug('Call API - Inner Method Run: ');
529
604
  messageResponse = createResponseMessage(method.responseID, true, response);
530
605
  requestQueue.resolveRequest(method.responseID, messageResponse);
531
606
  completeMethodRequestContext(method);
@@ -548,6 +623,7 @@ const onCallDevice = async (
548
623
 
549
624
  const runOptions: RunOptions = {
550
625
  keepSession: method.payload.keepSession,
626
+ skipInitialize: canSkipInitialize(method, device),
551
627
  ...parseInitOptions(method),
552
628
  };
553
629
  const deviceRun = () => device.run(inner, runOptions);
@@ -569,6 +645,9 @@ const onCallDevice = async (
569
645
  Log.debug('Call API - Run Error: ', error);
570
646
  completeMethodRequestContext(method, error);
571
647
  } finally {
648
+ // Release the pre-warm callback task so the next real call can proceed.
649
+ preWarmCallbackTask?.resolve();
650
+
572
651
  const response = messageResponse;
573
652
 
574
653
  if (response) {
@@ -698,23 +777,59 @@ function initDeviceForBle(method: BaseMethod) {
698
777
  }
699
778
 
700
779
  /**
701
- * If the Bluetooth connection times out, retry 6 times
780
+ * Check if we can skip initialize for this method
702
781
  */
703
- let bleTimeoutRetry = 0;
782
+ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
783
+ const reasons: string[] = [];
784
+ // Must have allowUsePreInitialize enabled on method (the safety gate:
785
+ // only sign-style methods opt in; getAddress/getPublicKey never do).
786
+ if (!method.allowUsePreInitialize) reasons.push('method.disallow');
787
+ // Caller must explicitly opt in per call (on-demand, more flexible).
788
+ if (!method.payload?.usePreInitialize) reasons.push('payload.usePreInitialize=false');
789
+ // Context must match (passphrase/deviceId)
790
+ if (!device.isPreInitializeMetaMatch(method.payload)) reasons.push('meta.mismatch');
791
+ // Device must have been initialized before (has features)
792
+ if (!device.features) reasons.push('features.missing');
793
+ // Must be within pre-initialize TTL
794
+ if (!device.isPreInitializedValid(PRE_INITIALIZE_TTL_MS)) reasons.push('ttl.expired');
795
+
796
+ if (reasons.length) {
797
+ Log.debug(`[PRE-INIT][MISS] method=${method.name} ${reasons.join(',')}`);
798
+ return false;
799
+ }
800
+
801
+ const savedMs = device.getLastInitializeDuration();
802
+ const saved = typeof savedMs === 'number' ? `saved ${savedMs}ms` : 'within TTL + meta match';
803
+ Log.debug(`[PRE-INIT][HIT] method=${method.name} skip Initialize (${saved})`);
704
804
 
705
- async function connectDeviceForBle(method: BaseMethod, device: Device) {
805
+ return true;
806
+ }
807
+
808
+ /**
809
+ * If the Bluetooth connection times out, retry up to 6 times
810
+ * @param retryCount - Current retry count (default 0)
811
+ */
812
+ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
706
813
  try {
707
814
  await device.acquire();
708
815
  if (method.payload?.onlyConnectBleDevice) {
709
816
  return;
710
817
  }
711
- await device.initialize(parseInitOptions(method));
818
+ // Skip initialize if conditions are met
819
+ if (!canSkipInitialize(method, device)) {
820
+ const initOptions = parseInitOptions(method);
821
+ await device.initialize(initOptions);
822
+ device.markPreInitialized({
823
+ passphraseState: initOptions.passphraseState,
824
+ deviceId: initOptions.deviceId,
825
+ });
826
+ }
712
827
  } catch (err) {
713
- if (err.errorCode === HardwareErrorCode.BleTimeoutError && bleTimeoutRetry <= 5) {
714
- bleTimeoutRetry += 1;
715
- Log.debug(`Bletooth connect timeout and will retry, retry count: ${bleTimeoutRetry}`);
828
+ if (err.errorCode === HardwareErrorCode.BleTimeoutError && retryCount < 6) {
829
+ const nextRetry = retryCount + 1;
830
+ Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
716
831
  await wait(3000);
717
- await connectDeviceForBle(method, device);
832
+ await connectDeviceForBle(method, device, nextRetry);
718
833
  } else {
719
834
  throw err;
720
835
  }
@@ -726,6 +841,7 @@ type IPollFn<T> = (time?: number) => T;
726
841
  const ensureConnected = async (
727
842
  _context: CoreContext,
728
843
  method: BaseMethod,
844
+ connectId: string,
729
845
  pollingId: number,
730
846
  abortSignal?: AbortSignal
731
847
  ) => {
@@ -757,7 +873,7 @@ const ensureConnected = async (
757
873
  return;
758
874
  }
759
875
 
760
- if (!pollingState[pollingId]) {
876
+ if (!pollingManager.isActive(connectId, pollingId)) {
761
877
  Log.debug('EnsureConnected function stop, polling id: ', pollingId);
762
878
  reject(ERRORS.TypedError(HardwareErrorCode.PollingStop));
763
879
  return;
@@ -815,8 +931,6 @@ const ensureConnected = async (
815
931
  * Bluetooth should call initialize here
816
932
  */
817
933
  if (DataManager.isBleConnect(env)) {
818
- bleTimeoutRetry = 0;
819
-
820
934
  if (abort()) {
821
935
  return;
822
936
  }
@@ -879,7 +993,7 @@ const ensureConnected = async (
879
993
  // eslint-disable-next-line no-promise-executor-return
880
994
  return setTimeout(() => resolve(poll(time * 1.5)), time);
881
995
  });
882
- pollingState[pollingId] = true;
996
+ // pollingManager.start(connectId) already registered this pollingId as active
883
997
  return poll();
884
998
  };
885
999
 
@@ -1013,6 +1127,7 @@ const onDeviceConnectHandler = (device: Device) => {
1013
1127
  };
1014
1128
 
1015
1129
  const onDeviceDisconnectHandler = (device: Device) => {
1130
+ device.clearPreInitialized();
1016
1131
  const env = DataManager.getSettings('env');
1017
1132
  const deviceObject = DataManager.isBleConnect(env) ? device : device.toMessageObject();
1018
1133
  postMessage(createDeviceMessage(DEVICE.DISCONNECT, { device: deviceObject as KnownDevice }));
@@ -1189,8 +1304,8 @@ export default class Core extends EventEmitter {
1189
1304
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
1190
1305
  this.requestQueue.registerPendingCallbackTask(connectId, callbackPromise);
1191
1306
  },
1192
- waitForCallbackTasks: (connectId: string) =>
1193
- this.requestQueue.waitForPendingCallbackTasks(connectId),
1307
+ waitForCallbackTasks: (connectId: string, exceptTask?: Deferred<void>) =>
1308
+ this.requestQueue.waitForPendingCallbackTasks(connectId, exceptTask),
1194
1309
  cancelCallbackTasks: (connectId: string) => this.requestQueue.cancelCallbackTasks(connectId),
1195
1310
  };
1196
1311
  }
@@ -1221,10 +1336,10 @@ export default class Core extends EventEmitter {
1221
1336
  }
1222
1337
 
1223
1338
  case IFRAME.CALL: {
1224
- Log.log('call API: ', message);
1339
+ Log.log(`[${Date.now()}][CALL_API]`, message);
1225
1340
  const response = await callAPI(this.getCoreContext(), message);
1226
1341
  const { success, payload } = response;
1227
- Log.log('call API Response: ', response);
1342
+ Log.log(`[${Date.now()}][CALL_API_RESPONSE]`, response);
1228
1343
  if (success) {
1229
1344
  return response;
1230
1345
  }
@@ -1257,6 +1372,9 @@ export default class Core extends EventEmitter {
1257
1372
  dispose() {
1258
1373
  _deviceList = undefined;
1259
1374
  _connector = undefined;
1375
+ deviceCacheMap.clear();
1376
+ preWarmInflight.clear();
1377
+ preWarmDoneAt.clear();
1260
1378
  Log.debug(`[Core] Disposing SDK instance: ${this.sdkInstanceId}`);
1261
1379
  cleanupSdkInstance(this.sdkInstanceId);
1262
1380
  }
@@ -62,6 +62,7 @@ export type InitOptions = {
62
62
 
63
63
  export type RunOptions = {
64
64
  keepSession?: boolean;
65
+ skipInitialize?: boolean;
65
66
  } & InitOptions;
66
67
 
67
68
  const parseRunOptions = (options?: RunOptions): RunOptions => {
@@ -196,6 +197,18 @@ export class Device extends EventEmitter {
196
197
 
197
198
  pendingCallbackPromise?: Deferred<void>;
198
199
 
200
+ /** Pre-initialize timestamp (ms) */
201
+ private preInitializedAt?: number;
202
+
203
+ /** Pre-initialize context, used to verify state consistency before skipping */
204
+ private preInitializeMeta?: {
205
+ passphraseState?: string;
206
+ deviceId?: string;
207
+ };
208
+
209
+ /** Last Initialize duration (ms), reported as "saved" when a skip happens */
210
+ private lastInitializeDurationMs?: number;
211
+
199
212
  constructor(descriptor: DeviceDescriptor, sdkInstanceId?: string) {
200
213
  super();
201
214
  this.originalDescriptor = descriptor;
@@ -355,6 +368,60 @@ export class Device extends EventEmitter {
355
368
  this.deviceAcquired = false;
356
369
  }
357
370
 
371
+ /**
372
+ * Pre-initialize: connect + Initialize ahead of the sign. Only runs the
373
+ * fallback init when features are missing (gate on `!this.features`, not
374
+ * isUsedHere which is always false on BLE); otherwise just records the mark.
375
+ */
376
+ async preInitialize(initOptions?: InitOptions) {
377
+ if (!this.features) {
378
+ await this.acquire();
379
+ await this.initialize(initOptions);
380
+ }
381
+ this.markPreInitialized({
382
+ passphraseState: initOptions?.passphraseState,
383
+ deviceId: initOptions?.deviceId,
384
+ });
385
+ }
386
+
387
+ markPreInitialized(meta?: { passphraseState?: string; deviceId?: string }) {
388
+ this.preInitializedAt = Date.now();
389
+ this.preInitializeMeta = meta
390
+ ? {
391
+ passphraseState: meta.passphraseState === '' ? undefined : meta.passphraseState,
392
+ deviceId: meta.deviceId === '' ? undefined : meta.deviceId,
393
+ }
394
+ : undefined;
395
+ }
396
+
397
+ clearPreInitialized() {
398
+ this.preInitializedAt = undefined;
399
+ this.preInitializeMeta = undefined;
400
+ }
401
+
402
+ isPreInitializeMetaMatch(payload?: { passphraseState?: string; deviceId?: string }) {
403
+ if (!this.preInitializeMeta) return true;
404
+ const passphraseState = payload?.passphraseState === '' ? undefined : payload?.passphraseState;
405
+ const deviceId = payload?.deviceId === '' ? undefined : payload?.deviceId;
406
+ return (
407
+ this.preInitializeMeta.passphraseState === passphraseState &&
408
+ this.preInitializeMeta.deviceId === deviceId
409
+ );
410
+ }
411
+
412
+ isPreInitializedValid(ttlMs: number) {
413
+ if (!this.preInitializedAt) return false;
414
+ return Date.now() - this.preInitializedAt <= ttlMs;
415
+ }
416
+
417
+ setLastInitializeDuration(durationMs: number) {
418
+ this.lastInitializeDurationMs = durationMs;
419
+ }
420
+
421
+ getLastInitializeDuration() {
422
+ return this.lastInitializeDurationMs;
423
+ }
424
+
358
425
  getCommands() {
359
426
  return this.commands;
360
427
  }
@@ -482,13 +549,7 @@ export class Device extends EventEmitter {
482
549
  payload.passphrase_state = options?.passphraseState;
483
550
  payload.is_contains_attach = true;
484
551
 
485
- Log.debug('Initialize device begin:', {
486
- deviceId: options?.deviceId,
487
- passphraseState: options?.passphraseState,
488
- initSession: options?.initSession,
489
- InitializePayload: payload,
490
- });
491
-
552
+ const initStartAt = Date.now();
492
553
  try {
493
554
  // @ts-expect-error
494
555
  const { message } = await Promise.race([
@@ -501,7 +562,8 @@ export class Device extends EventEmitter {
501
562
  }),
502
563
  ]);
503
564
 
504
- Log.debug('Initialize device end: ', message);
565
+ const initCostMs = Date.now() - initStartAt;
566
+ this.setLastInitializeDuration(initCostMs);
505
567
  this._updateFeatures(message, options?.initSession);
506
568
  await TransportManager.reconfigure(this.features);
507
569
  } catch (error) {
@@ -588,7 +650,9 @@ export class Device extends EventEmitter {
588
650
 
589
651
  try {
590
652
  if (fn) {
591
- await this.initialize(options);
653
+ if (!options?.skipInitialize) {
654
+ await this.initialize(options);
655
+ }
592
656
  }
593
657
  } catch (error) {
594
658
  this.runPromise = null;
package/src/inject.ts CHANGED
@@ -142,7 +142,7 @@ export const createCoreApi = (
142
142
 
143
143
  testInitializeDeviceDuration: (connectId, params) =>
144
144
  call({ ...params, connectId, method: 'testInitializeDeviceDuration' }),
145
-
145
+ preInitialize: (connectId, params) => call({ ...params, connectId, method: 'preInitialize' }),
146
146
  deviceBackup: connectId => call({ connectId, method: 'deviceBackup' }),
147
147
  deviceChangePin: (connectId, params) => call({ ...params, connectId, method: 'deviceChangePin' }),
148
148
  deviceFlags: (connectId, params) => call({ ...params, connectId, method: 'deviceFlags' }),
@@ -2,6 +2,7 @@ import type { off, on, removeAllListeners } from './event';
2
2
  import type { uiResponse } from './uiResponse';
3
3
  import type { init, updateSettings } from './init';
4
4
  import type { testInitializeDeviceDuration } from './testInitializeDeviceDuration';
5
+ import type { preInitialize } from './preInitialize';
5
6
  import type { getLogs } from './getLogs';
6
7
  import type { checkBridgeStatus } from './checkBridgeStatus';
7
8
  import type { checkBridgeRelease } from './checkBridgeRelease';
@@ -152,6 +153,7 @@ export type CoreApi = {
152
153
  * Test function
153
154
  */
154
155
  testInitializeDeviceDuration: typeof testInitializeDeviceDuration;
156
+ preInitialize: typeof preInitialize;
155
157
 
156
158
  /**
157
159
  * 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>;
@@ -47,6 +47,11 @@ export interface CommonParams {
47
47
  * Only connect device, not initialize device, only ble connect
48
48
  */
49
49
  onlyConnectBleDevice?: boolean;
50
+
51
+ /**
52
+ * Use pre-initialized device state (BLE only)
53
+ */
54
+ usePreInitialize?: boolean;
50
55
  }
51
56
 
52
57
  export type Params<T> = CommonParams & T & { bundle?: undefined };