@onekeyfe/hd-core 1.1.26 → 1.1.27-alpha.100

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 (130) hide show
  1. package/__tests__/preInitialize.test.ts +22 -0
  2. package/dist/api/BaseMethod.d.ts +4 -0
  3. package/dist/api/BaseMethod.d.ts.map +1 -1
  4. package/dist/api/alephium/AlephiumSignMessage.d.ts.map +1 -1
  5. package/dist/api/alephium/AlephiumSignTransaction.d.ts.map +1 -1
  6. package/dist/api/algo/AlgoSignTransaction.d.ts.map +1 -1
  7. package/dist/api/aptos/AptosSignInMessage.d.ts.map +1 -1
  8. package/dist/api/aptos/AptosSignMessage.d.ts.map +1 -1
  9. package/dist/api/aptos/AptosSignTransaction.d.ts.map +1 -1
  10. package/dist/api/benfen/BenfenSignMessage.d.ts.map +1 -1
  11. package/dist/api/benfen/BenfenSignTransaction.d.ts.map +1 -1
  12. package/dist/api/btc/BTCSignMessage.d.ts.map +1 -1
  13. package/dist/api/btc/BTCSignPsbt.d.ts.map +1 -1
  14. package/dist/api/btc/BTCSignTransaction.d.ts.map +1 -1
  15. package/dist/api/cardano/CardanoSignMessage.d.ts.map +1 -1
  16. package/dist/api/cardano/CardanoSignTransaction.d.ts.map +1 -1
  17. package/dist/api/conflux/ConfluxSignMessage.d.ts.map +1 -1
  18. package/dist/api/conflux/ConfluxSignMessageCIP23.d.ts.map +1 -1
  19. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  20. package/dist/api/cosmos/CosmosSignTransaction.d.ts.map +1 -1
  21. package/dist/api/device/PreInitialize.d.ts +6 -0
  22. package/dist/api/device/PreInitialize.d.ts.map +1 -0
  23. package/dist/api/dynex/DnxSignTransaction.d.ts.map +1 -1
  24. package/dist/api/evm/EVMSignMessage.d.ts.map +1 -1
  25. package/dist/api/evm/EVMSignMessageEIP712.d.ts.map +1 -1
  26. package/dist/api/evm/EVMSignTransaction.d.ts.map +1 -1
  27. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  28. package/dist/api/filecoin/FilecoinSignTransaction.d.ts.map +1 -1
  29. package/dist/api/index.d.ts +1 -0
  30. package/dist/api/index.d.ts.map +1 -1
  31. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  32. package/dist/api/near/NearSignTransaction.d.ts.map +1 -1
  33. package/dist/api/nem/NEMSignTransaction.d.ts.map +1 -1
  34. package/dist/api/neo/NeoSignTransaction.d.ts.map +1 -1
  35. package/dist/api/nervos/NervosSignTransaction.d.ts.map +1 -1
  36. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  37. package/dist/api/nostr/NostrSignEvent.d.ts.map +1 -1
  38. package/dist/api/nostr/NostrSignSchnorr.d.ts.map +1 -1
  39. package/dist/api/polkadot/PolkadotSignTransaction.d.ts.map +1 -1
  40. package/dist/api/scdo/ScdoSignMessage.d.ts.map +1 -1
  41. package/dist/api/scdo/ScdoSignTransaction.d.ts.map +1 -1
  42. package/dist/api/solana/SolSignMessage.d.ts.map +1 -1
  43. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  44. package/dist/api/solana/SolSignTransaction.d.ts.map +1 -1
  45. package/dist/api/starcoin/StarcoinSignMessage.d.ts.map +1 -1
  46. package/dist/api/starcoin/StarcoinSignTransaction.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts.map +1 -1
  48. package/dist/api/sui/SuiSignMessage.d.ts.map +1 -1
  49. package/dist/api/sui/SuiSignTransaction.d.ts.map +1 -1
  50. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  51. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  52. package/dist/api/ton/TonSignProof.d.ts.map +1 -1
  53. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  54. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  55. package/dist/api/xrp/XrpSignTransaction.d.ts.map +1 -1
  56. package/dist/core/PollingStateManager.d.ts +8 -0
  57. package/dist/core/PollingStateManager.d.ts.map +1 -0
  58. package/dist/core/RequestQueue.d.ts +1 -1
  59. package/dist/core/RequestQueue.d.ts.map +1 -1
  60. package/dist/core/index.d.ts.map +1 -1
  61. package/dist/device/Device.d.ts +15 -0
  62. package/dist/device/Device.d.ts.map +1 -1
  63. package/dist/index.d.ts +19 -0
  64. package/dist/index.js +345 -113
  65. package/dist/types/api/index.d.ts +2 -0
  66. package/dist/types/api/index.d.ts.map +1 -1
  67. package/dist/types/api/preInitialize.d.ts +3 -0
  68. package/dist/types/api/preInitialize.d.ts.map +1 -0
  69. package/dist/types/params.d.ts +1 -0
  70. package/dist/types/params.d.ts.map +1 -1
  71. package/package.json +4 -4
  72. package/src/api/BaseMethod.ts +22 -0
  73. package/src/api/alephium/AlephiumSignMessage.ts +1 -0
  74. package/src/api/alephium/AlephiumSignTransaction.ts +1 -0
  75. package/src/api/algo/AlgoSignTransaction.ts +1 -0
  76. package/src/api/aptos/AptosSignInMessage.ts +1 -0
  77. package/src/api/aptos/AptosSignMessage.ts +1 -0
  78. package/src/api/aptos/AptosSignTransaction.ts +1 -0
  79. package/src/api/benfen/BenfenSignMessage.ts +1 -0
  80. package/src/api/benfen/BenfenSignTransaction.ts +1 -0
  81. package/src/api/btc/BTCSignMessage.ts +1 -0
  82. package/src/api/btc/BTCSignPsbt.ts +1 -0
  83. package/src/api/btc/BTCSignTransaction.ts +1 -0
  84. package/src/api/cardano/CardanoSignMessage.ts +1 -0
  85. package/src/api/cardano/CardanoSignTransaction.ts +1 -0
  86. package/src/api/conflux/ConfluxSignMessage.ts +1 -0
  87. package/src/api/conflux/ConfluxSignMessageCIP23.ts +1 -0
  88. package/src/api/conflux/ConfluxSignTransaction.ts +1 -0
  89. package/src/api/cosmos/CosmosSignTransaction.ts +1 -0
  90. package/src/api/device/PreInitialize.ts +41 -0
  91. package/src/api/dynex/DnxSignTransaction.ts +1 -0
  92. package/src/api/evm/EVMSignMessage.ts +2 -0
  93. package/src/api/evm/EVMSignMessageEIP712.ts +1 -0
  94. package/src/api/evm/EVMSignTransaction.ts +2 -0
  95. package/src/api/evm/EVMSignTypedData.ts +2 -0
  96. package/src/api/filecoin/FilecoinSignTransaction.ts +1 -0
  97. package/src/api/index.ts +1 -0
  98. package/src/api/kaspa/KaspaSignTransaction.ts +1 -0
  99. package/src/api/near/NearSignTransaction.ts +1 -0
  100. package/src/api/nem/NEMSignTransaction.ts +1 -0
  101. package/src/api/neo/NeoSignTransaction.ts +1 -0
  102. package/src/api/nervos/NervosSignTransaction.ts +1 -0
  103. package/src/api/nexa/NexaSignTransaction.ts +2 -0
  104. package/src/api/nostr/NostrSignEvent.ts +1 -0
  105. package/src/api/nostr/NostrSignSchnorr.ts +1 -0
  106. package/src/api/polkadot/PolkadotSignTransaction.ts +1 -0
  107. package/src/api/scdo/ScdoSignMessage.ts +1 -0
  108. package/src/api/scdo/ScdoSignTransaction.ts +1 -0
  109. package/src/api/solana/SolSignMessage.ts +1 -0
  110. package/src/api/solana/SolSignOffchainMessage.ts +1 -0
  111. package/src/api/solana/SolSignTransaction.ts +1 -0
  112. package/src/api/starcoin/StarcoinSignMessage.ts +1 -0
  113. package/src/api/starcoin/StarcoinSignTransaction.ts +1 -0
  114. package/src/api/stellar/StellarSignTransaction.ts +1 -0
  115. package/src/api/sui/SuiSignMessage.ts +1 -0
  116. package/src/api/sui/SuiSignTransaction.ts +1 -0
  117. package/src/api/ton/TonSignData.ts +1 -0
  118. package/src/api/ton/TonSignMessage.ts +1 -0
  119. package/src/api/ton/TonSignProof.ts +1 -0
  120. package/src/api/tron/TronSignMessage.ts +1 -0
  121. package/src/api/tron/TronSignTransaction.ts +1 -0
  122. package/src/api/xrp/XrpSignTransaction.ts +1 -0
  123. package/src/core/PollingStateManager.ts +47 -0
  124. package/src/core/RequestQueue.ts +10 -3
  125. package/src/core/index.ts +152 -27
  126. package/src/device/Device.ts +66 -9
  127. package/src/inject.ts +1 -1
  128. package/src/types/api/index.ts +2 -0
  129. package/src/types/api/preInitialize.ts +3 -0
  130. package/src/types/params.ts +5 -0
@@ -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,63 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
206
212
  return createResponseMessage(method.responseID, false, { error });
207
213
  }
208
214
 
215
+ // only the pre-warm signal (PreInitialize) forks here; normal methods fall
216
+ // through to onCallDevice below, so the pre-warm dedup/guards never touch them
217
+ if (method.isPreWarmSignal) {
218
+ return handlePreWarmSignal(context, message, method);
219
+ }
220
+
209
221
  return onCallDevice(context, message, method);
210
222
  };
211
223
 
224
+ // Wrapper for "pre-warm signal" methods: coalesce in-flight same-key pre-warm,
225
+ // skip if warmed within TTL, else run + track. The "hang up so the next real
226
+ // call waits" part lives in onCallDevice (setPrePendingCallPromise).
227
+ const handlePreWarmSignal = async (
228
+ context: CoreContext,
229
+ message: CoreMessage,
230
+ method: BaseMethod
231
+ ): Promise<any> => {
232
+ // no connectId: can't target a device safely, skip pre-warm (ack only)
233
+ if (!method.connectId) {
234
+ return createResponseMessage(method.responseID, true, true);
235
+ }
236
+
237
+ const key = method.getPreWarmKey();
238
+
239
+ const inflight = preWarmInflight.get(key);
240
+ if (inflight) {
241
+ // reply with THIS call's responseID (not the other call's response object)
242
+ try {
243
+ await inflight;
244
+ } catch {
245
+ // pre-warm is best-effort; ignore its failure for the coalesced caller
246
+ }
247
+ return createResponseMessage(method.responseID, true, true);
248
+ }
249
+
250
+ const doneAt = preWarmDoneAt.get(key);
251
+ if (typeof doneAt === 'number' && Date.now() - doneAt <= method.preWarmTtl) {
252
+ return createResponseMessage(method.responseID, true, true);
253
+ }
254
+
255
+ const run = onCallDevice(context, message, method);
256
+ preWarmInflight.set(key, run);
257
+ try {
258
+ const result = await run;
259
+ // Only remember the warm if it actually succeeded — a failed pre-warm must
260
+ // not suppress the next pre-warm within the TTL.
261
+ if (result?.success === true && result?.payload === true) {
262
+ preWarmDoneAt.set(key, Date.now());
263
+ }
264
+ return result;
265
+ } finally {
266
+ if (preWarmInflight.get(key) === run) {
267
+ preWarmInflight.delete(key);
268
+ }
269
+ }
270
+ };
271
+
212
272
  const waitWithTimeout = async (promise: Promise<any>, timeout: number) => {
213
273
  const timeoutPromise = new Promise((_, reject) => {
214
274
  setTimeout(() => reject(new Error('Request timeout')), timeout);
@@ -244,7 +304,15 @@ const onCallDevice = async (
244
304
 
245
305
  updateMethodRequestContext(method, { status: 'running' });
246
306
 
247
- const connectStateChange = preConnectCache.passphraseState !== method.payload.passphraseState;
307
+ // Normalize undefined / null / '' to '' — they all mean "main wallet, no
308
+ // passphrase". Without this, the first call (preConnectCache starts undefined)
309
+ // or any '' call after a non-'' one is wrongly treated as a passphrase switch
310
+ // and needlessly clears the device cache -> forces a re-enumeration Initialize.
311
+ // A real switch ('' <-> 'stateX', or 'stateX' <-> 'stateY') still differs.
312
+ const normalizePassphraseState = (s?: string | null) => s || '';
313
+ const connectStateChange =
314
+ normalizePassphraseState(preConnectCache.passphraseState) !==
315
+ normalizePassphraseState(method.payload.passphraseState);
248
316
 
249
317
  preConnectCache = {
250
318
  passphraseState: method.payload.passphraseState,
@@ -264,18 +332,31 @@ const onCallDevice = async (
264
332
 
265
333
  const task = requestQueue.createTask(method);
266
334
 
335
+ // Pre-warm holds the device as a per-connectId callback task so a concurrent
336
+ // real call waits (before ensureConnected) instead of racing its Initialize.
337
+ // Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
338
+ let preWarmCallbackTask: Deferred<void> | undefined;
339
+ if (method.isPreWarmSignal && method.connectId) {
340
+ preWarmCallbackTask = createDeferred<void>();
341
+ context.registerCallbackTask(method.connectId, preWarmCallbackTask);
342
+ }
343
+
267
344
  let device: Device;
268
345
  try {
269
346
  /**
270
347
  * Polling to ensure successful connection
271
348
  */
272
- if (pollingState[pollingId]) {
273
- pollingState[pollingId] = false;
274
- }
275
- pollingId += 1;
276
-
277
- device = await ensureConnected(context, method, pollingId, task.abortController?.signal);
349
+ const connectId = method.connectId ?? '';
350
+ const pollingId = pollingManager.start(connectId);
351
+ device = await ensureConnected(
352
+ context,
353
+ method,
354
+ connectId,
355
+ pollingId,
356
+ task.abortController?.signal
357
+ );
278
358
  } catch (e) {
359
+ preWarmCallbackTask?.resolve();
279
360
  console.log('ensureConnected error: ', e);
280
361
 
281
362
  completeMethodRequestContext(method, e);
@@ -291,6 +372,7 @@ const onCallDevice = async (
291
372
  }
292
373
 
293
374
  if (method.payload?.onlyConnectBleDevice) {
375
+ preWarmCallbackTask?.resolve();
294
376
  Log.debug('Call API - only connect ble device: ', device?.mainId);
295
377
  return createResponseMessage(method.responseID, true, null);
296
378
  }
@@ -330,8 +412,9 @@ const onCallDevice = async (
330
412
  );
331
413
 
332
414
  try {
415
+ // Wait for any pending task except our own (self-wait would deadlock).
333
416
  if (method.connectId) {
334
- await context.waitForCallbackTasks(method.connectId);
417
+ await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
335
418
  }
336
419
 
337
420
  await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
@@ -525,7 +608,6 @@ const onCallDevice = async (
525
608
 
526
609
  try {
527
610
  const response: object = await method.run();
528
- Log.debug('Call API - Inner Method Run: ');
529
611
  messageResponse = createResponseMessage(method.responseID, true, response);
530
612
  requestQueue.resolveRequest(method.responseID, messageResponse);
531
613
  completeMethodRequestContext(method);
@@ -548,6 +630,7 @@ const onCallDevice = async (
548
630
 
549
631
  const runOptions: RunOptions = {
550
632
  keepSession: method.payload.keepSession,
633
+ skipInitialize: canSkipInitialize(method, device),
551
634
  ...parseInitOptions(method),
552
635
  };
553
636
  const deviceRun = () => device.run(inner, runOptions);
@@ -569,6 +652,9 @@ const onCallDevice = async (
569
652
  Log.debug('Call API - Run Error: ', error);
570
653
  completeMethodRequestContext(method, error);
571
654
  } finally {
655
+ // Release the pre-warm callback task so the next real call can proceed.
656
+ preWarmCallbackTask?.resolve();
657
+
572
658
  const response = messageResponse;
573
659
 
574
660
  if (response) {
@@ -698,23 +784,59 @@ function initDeviceForBle(method: BaseMethod) {
698
784
  }
699
785
 
700
786
  /**
701
- * If the Bluetooth connection times out, retry 6 times
787
+ * Check if we can skip initialize for this method
702
788
  */
703
- let bleTimeoutRetry = 0;
789
+ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
790
+ const reasons: string[] = [];
791
+ // only sign-style methods opt in; getAddress/getPublicKey never do
792
+ if (!method.allowUsePreInitialize) reasons.push('method.disallow');
793
+ // caller must opt in per call
794
+ if (!method.payload?.usePreInitialize) reasons.push('payload.usePreInitialize=false');
795
+ // no connectId: can't pin the target device, never skip
796
+ if (!method.connectId) reasons.push('connectId.missing');
797
+ // passphrase state must match the pre-initialize
798
+ if (!device.isPreInitializeMetaMatch(method.payload)) reasons.push('meta.mismatch');
799
+ // device must have been initialized before (has features)
800
+ if (!device.features) reasons.push('features.missing');
801
+ // within pre-initialize TTL
802
+ if (!device.isPreInitializedValid(PRE_INITIALIZE_TTL_MS)) reasons.push('ttl.expired');
803
+
804
+ if (reasons.length) {
805
+ Log.debug(`[PRE-INIT][MISS] method=${method.name} ${reasons.join(',')}`);
806
+ return false;
807
+ }
808
+
809
+ const savedMs = device.getLastInitializeDuration();
810
+ const saved = typeof savedMs === 'number' ? `saved ${savedMs}ms` : 'within TTL + meta match';
811
+ Log.debug(`[PRE-INIT][HIT] method=${method.name} skip Initialize (${saved})`);
812
+
813
+ return true;
814
+ }
704
815
 
705
- async function connectDeviceForBle(method: BaseMethod, device: Device) {
816
+ /**
817
+ * If the Bluetooth connection times out, retry up to 6 times
818
+ * @param retryCount - Current retry count (default 0)
819
+ */
820
+ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
706
821
  try {
707
822
  await device.acquire();
708
823
  if (method.payload?.onlyConnectBleDevice) {
709
824
  return;
710
825
  }
711
- await device.initialize(parseInitOptions(method));
826
+ // Skip initialize if conditions are met
827
+ if (!canSkipInitialize(method, device)) {
828
+ const initOptions = parseInitOptions(method);
829
+ await device.initialize(initOptions);
830
+ device.markPreInitialized({
831
+ passphraseState: initOptions.passphraseState,
832
+ });
833
+ }
712
834
  } 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}`);
835
+ if (err.errorCode === HardwareErrorCode.BleTimeoutError && retryCount < 6) {
836
+ const nextRetry = retryCount + 1;
837
+ Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
716
838
  await wait(3000);
717
- await connectDeviceForBle(method, device);
839
+ await connectDeviceForBle(method, device, nextRetry);
718
840
  } else {
719
841
  throw err;
720
842
  }
@@ -726,6 +848,7 @@ type IPollFn<T> = (time?: number) => T;
726
848
  const ensureConnected = async (
727
849
  _context: CoreContext,
728
850
  method: BaseMethod,
851
+ connectId: string,
729
852
  pollingId: number,
730
853
  abortSignal?: AbortSignal
731
854
  ) => {
@@ -757,7 +880,7 @@ const ensureConnected = async (
757
880
  return;
758
881
  }
759
882
 
760
- if (!pollingState[pollingId]) {
883
+ if (!pollingManager.isActive(connectId, pollingId)) {
761
884
  Log.debug('EnsureConnected function stop, polling id: ', pollingId);
762
885
  reject(ERRORS.TypedError(HardwareErrorCode.PollingStop));
763
886
  return;
@@ -815,8 +938,6 @@ const ensureConnected = async (
815
938
  * Bluetooth should call initialize here
816
939
  */
817
940
  if (DataManager.isBleConnect(env)) {
818
- bleTimeoutRetry = 0;
819
-
820
941
  if (abort()) {
821
942
  return;
822
943
  }
@@ -879,7 +1000,7 @@ const ensureConnected = async (
879
1000
  // eslint-disable-next-line no-promise-executor-return
880
1001
  return setTimeout(() => resolve(poll(time * 1.5)), time);
881
1002
  });
882
- pollingState[pollingId] = true;
1003
+ // pollingManager.start(connectId) already registered this pollingId as active
883
1004
  return poll();
884
1005
  };
885
1006
 
@@ -1013,6 +1134,7 @@ const onDeviceConnectHandler = (device: Device) => {
1013
1134
  };
1014
1135
 
1015
1136
  const onDeviceDisconnectHandler = (device: Device) => {
1137
+ device.clearPreInitialized();
1016
1138
  const env = DataManager.getSettings('env');
1017
1139
  const deviceObject = DataManager.isBleConnect(env) ? device : device.toMessageObject();
1018
1140
  postMessage(createDeviceMessage(DEVICE.DISCONNECT, { device: deviceObject as KnownDevice }));
@@ -1189,8 +1311,8 @@ export default class Core extends EventEmitter {
1189
1311
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
1190
1312
  this.requestQueue.registerPendingCallbackTask(connectId, callbackPromise);
1191
1313
  },
1192
- waitForCallbackTasks: (connectId: string) =>
1193
- this.requestQueue.waitForPendingCallbackTasks(connectId),
1314
+ waitForCallbackTasks: (connectId: string, exceptTask?: Deferred<void>) =>
1315
+ this.requestQueue.waitForPendingCallbackTasks(connectId, exceptTask),
1194
1316
  cancelCallbackTasks: (connectId: string) => this.requestQueue.cancelCallbackTasks(connectId),
1195
1317
  };
1196
1318
  }
@@ -1221,10 +1343,10 @@ export default class Core extends EventEmitter {
1221
1343
  }
1222
1344
 
1223
1345
  case IFRAME.CALL: {
1224
- Log.log('call API: ', message);
1346
+ Log.log(`[${Date.now()}][CALL_API]`, message);
1225
1347
  const response = await callAPI(this.getCoreContext(), message);
1226
1348
  const { success, payload } = response;
1227
- Log.log('call API Response: ', response);
1349
+ Log.log(`[${Date.now()}][CALL_API_RESPONSE]`, response);
1228
1350
  if (success) {
1229
1351
  return response;
1230
1352
  }
@@ -1257,6 +1379,9 @@ export default class Core extends EventEmitter {
1257
1379
  dispose() {
1258
1380
  _deviceList = undefined;
1259
1381
  _connector = undefined;
1382
+ deviceCacheMap.clear();
1383
+ preWarmInflight.clear();
1384
+ preWarmDoneAt.clear();
1260
1385
  Log.debug(`[Core] Disposing SDK instance: ${this.sdkInstanceId}`);
1261
1386
  cleanupSdkInstance(this.sdkInstanceId);
1262
1387
  }
@@ -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,17 @@ 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
+ };
207
+
208
+ /** Last Initialize duration (ms), reported as "saved" when a skip happens */
209
+ private lastInitializeDurationMs?: number;
210
+
199
211
  constructor(descriptor: DeviceDescriptor, sdkInstanceId?: string) {
200
212
  super();
201
213
  this.originalDescriptor = descriptor;
@@ -355,6 +367,54 @@ export class Device extends EventEmitter {
355
367
  this.deviceAcquired = false;
356
368
  }
357
369
 
370
+ /**
371
+ * Pre-initialize: connect + Initialize ahead of the sign. Only runs the
372
+ * fallback init when features are missing (gate on `!this.features`, not
373
+ * isUsedHere which is always false on BLE); otherwise just records the mark.
374
+ */
375
+ async preInitialize(initOptions?: InitOptions) {
376
+ if (!this.features) {
377
+ await this.acquire();
378
+ await this.initialize(initOptions);
379
+ }
380
+ this.markPreInitialized({
381
+ passphraseState: initOptions?.passphraseState,
382
+ });
383
+ }
384
+
385
+ markPreInitialized(meta?: { passphraseState?: string }) {
386
+ this.preInitializedAt = Date.now();
387
+ this.preInitializeMeta = meta
388
+ ? {
389
+ passphraseState: meta.passphraseState === '' ? undefined : meta.passphraseState,
390
+ }
391
+ : undefined;
392
+ }
393
+
394
+ clearPreInitialized() {
395
+ this.preInitializedAt = undefined;
396
+ this.preInitializeMeta = undefined;
397
+ }
398
+
399
+ isPreInitializeMetaMatch(payload?: { passphraseState?: string }) {
400
+ if (!this.preInitializeMeta) return true;
401
+ const passphraseState = payload?.passphraseState === '' ? undefined : payload?.passphraseState;
402
+ return this.preInitializeMeta.passphraseState === passphraseState;
403
+ }
404
+
405
+ isPreInitializedValid(ttlMs: number) {
406
+ if (!this.preInitializedAt) return false;
407
+ return Date.now() - this.preInitializedAt <= ttlMs;
408
+ }
409
+
410
+ setLastInitializeDuration(durationMs: number) {
411
+ this.lastInitializeDurationMs = durationMs;
412
+ }
413
+
414
+ getLastInitializeDuration() {
415
+ return this.lastInitializeDurationMs;
416
+ }
417
+
358
418
  getCommands() {
359
419
  return this.commands;
360
420
  }
@@ -482,13 +542,7 @@ export class Device extends EventEmitter {
482
542
  payload.passphrase_state = options?.passphraseState;
483
543
  payload.is_contains_attach = true;
484
544
 
485
- Log.debug('Initialize device begin:', {
486
- deviceId: options?.deviceId,
487
- passphraseState: options?.passphraseState,
488
- initSession: options?.initSession,
489
- InitializePayload: payload,
490
- });
491
-
545
+ const initStartAt = Date.now();
492
546
  try {
493
547
  // @ts-expect-error
494
548
  const { message } = await Promise.race([
@@ -501,7 +555,8 @@ export class Device extends EventEmitter {
501
555
  }),
502
556
  ]);
503
557
 
504
- Log.debug('Initialize device end: ', message);
558
+ const initCostMs = Date.now() - initStartAt;
559
+ this.setLastInitializeDuration(initCostMs);
505
560
  this._updateFeatures(message, options?.initSession);
506
561
  await TransportManager.reconfigure(this.features);
507
562
  } catch (error) {
@@ -588,7 +643,9 @@ export class Device extends EventEmitter {
588
643
 
589
644
  try {
590
645
  if (fn) {
591
- await this.initialize(options);
646
+ if (!options?.skipInitialize) {
647
+ await this.initialize(options);
648
+ }
592
649
  }
593
650
  } catch (error) {
594
651
  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 };