@onekeyfe/hd-core 1.2.0-alpha.134 → 1.2.0-alpha.136

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.0-alpha.134",
3
+ "version": "1.2.0-alpha.136",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.0-alpha.134",
29
- "@onekeyfe/hd-transport": "1.2.0-alpha.134",
28
+ "@onekeyfe/hd-shared": "1.2.0-alpha.136",
29
+ "@onekeyfe/hd-transport": "1.2.0-alpha.136",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "buffer": "^6.0.3",
@@ -46,5 +46,5 @@
46
46
  "@types/w3c-web-usb": "^1.0.10",
47
47
  "@types/web-bluetooth": "^0.0.21"
48
48
  },
49
- "gitHead": "47de2b0244584f6a541a84b0f94e4676bf63221a"
49
+ "gitHead": "92945bc4aae3d6602dd44c2e028e902e6bce7b55"
50
50
  }
@@ -168,8 +168,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
168
168
  async run() {
169
169
  try {
170
170
  if (this.device.isProtocolV2()) {
171
- const refreshStatusAndSettings = () =>
172
- this.device.getDeviceState({ refreshSections: ['status', 'settings'] });
171
+ const refreshStatusAndSettings = () => this.device.refreshProtocolV2SettingsAfterMutation();
173
172
  assertSettingsSupported(this.payload, DEVICE_SETTINGS_V1_ONLY_FIELDS, 'Protocol V2');
174
173
  const capabilities = getDeviceSettingsCapabilities(
175
174
  this.device.getCurrentDeviceType(),
@@ -201,20 +200,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
201
200
  const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
202
201
  page: DeviceSettingsPage.DevicePassphrase,
203
202
  });
204
- const updated = await refreshStatusAndSettings();
205
- const lockedAfterDisabling =
206
- requestedPassphrase === false &&
207
- current.status.unlocked === true &&
208
- updated.status.unlocked === false;
209
- if (
210
- updated.status.passphraseProtection !== requestedPassphrase &&
211
- !lockedAfterDisabling
212
- ) {
213
- throw TypedError(
214
- HardwareErrorCode.RuntimeError,
215
- 'Protocol V2 passphrase setting did not reach the requested value.'
216
- );
217
- }
203
+ await refreshStatusAndSettings();
218
204
  return res.message;
219
205
  }
220
206
  if (requestedAirgap !== undefined) {
@@ -8,6 +8,7 @@ import { invalidParameter } from '../helpers/filesystemValidation';
8
8
  import { writeProtocolV2File } from '../helpers/protocolV2FileWrite';
9
9
  import { UI_REQUEST, createUiMessage } from '../../events/ui-request';
10
10
  import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
11
+ import { LoggerNames, getLogger } from '../../utils';
11
12
  import {
12
13
  PRO2_WALLPAPER_HEIGHT,
13
14
  PRO2_WALLPAPER_WIDTH,
@@ -33,6 +34,7 @@ const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
33
34
  const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
34
35
  const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
35
36
  const FILESYSTEM_DIR_MAKE_MESSAGE_TYPE = 60809;
37
+ const Log = getLogger(LoggerNames.Method);
36
38
 
37
39
  function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
38
40
  if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) {
@@ -143,6 +145,15 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
143
145
  const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
144
146
  settings: { wallpaper_path: this.path },
145
147
  });
148
+ try {
149
+ await this.device.refreshProtocolV2SettingsAfterMutation();
150
+ } catch (error) {
151
+ // The wallpaper is already applied. A transient read-back failure must not
152
+ // make callers retry the completed file upload.
153
+ Log.warn('Protocol V2 wallpaper settings refresh failed after apply', {
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ }
146
157
  return {
147
158
  path: this.path,
148
159
  size: encoded.data.byteLength,
package/src/core/index.ts CHANGED
@@ -83,6 +83,7 @@ import type { BaseMethod } from '../api/BaseMethod';
83
83
 
84
84
  const Log = getLogger(LoggerNames.Core);
85
85
  const PRE_INITIALIZE_TTL_MS = 60 * 1000;
86
+ const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 1000;
86
87
 
87
88
  // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
88
89
  // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
@@ -296,26 +297,35 @@ const handlePreWarmSignal = async (
296
297
  }
297
298
  };
298
299
 
299
- const waitWithTimeout = async (promise: Promise<any>, timeout: number) => {
300
- const timeoutPromise = new Promise((_, reject) => {
301
- setTimeout(() => reject(new Error('Request timeout')), timeout);
302
- });
303
- return Promise.race([promise, timeoutPromise]);
304
- };
305
-
306
300
  const waitForPendingPromise = async (
307
- getPrePendingCallPromise: () => Promise<void> | undefined,
308
- removePrePendingCallPromise?: (promise: Promise<void> | undefined) => void
301
+ connectId: string,
302
+ getPrePendingCallPromise: (connectId: string) => Promise<void> | undefined,
303
+ removePrePendingCallPromise?: (connectId: string, promise: Promise<void>) => void
309
304
  ) => {
310
- const pendingPromise = getPrePendingCallPromise();
305
+ const pendingPromise = getPrePendingCallPromise(connectId);
311
306
  if (pendingPromise) {
312
307
  Log.debug('pre pending call promise before call method, wait for it');
308
+ let timer: ReturnType<typeof setTimeout> | undefined;
309
+ let timedOut = false;
313
310
  try {
314
- await waitWithTimeout(pendingPromise, 5 * 1000);
311
+ await Promise.race([
312
+ pendingPromise,
313
+ new Promise<void>(resolve => {
314
+ timer = setTimeout(() => {
315
+ timedOut = true;
316
+ resolve();
317
+ }, PRE_PENDING_CALL_TIMEOUT_MS);
318
+ }),
319
+ ]);
315
320
  } catch (error) {
316
- // ignore timeout error
321
+ // Cancellation is best-effort; the transport teardown owns recovery.
322
+ } finally {
323
+ if (timer) clearTimeout(timer);
324
+ removePrePendingCallPromise?.(connectId, pendingPromise);
325
+ }
326
+ if (timedOut) {
327
+ Log.warn('pre pending call promise timed out before call method', { connectId });
317
328
  }
318
- removePrePendingCallPromise?.(pendingPromise);
319
329
  Log.debug('pre pending call promise before call method done');
320
330
  }
321
331
  };
@@ -327,7 +337,7 @@ const onCallDevice = async (
327
337
  ): Promise<any> => {
328
338
  let messageResponse: any;
329
339
 
330
- const { requestQueue, getPrePendingCallPromise, setPrePendingCallPromise } = context;
340
+ const { requestQueue, getPrePendingCallPromise, removePrePendingCallPromise } = context;
331
341
 
332
342
  updateMethodRequestContext(method, { status: 'running' });
333
343
 
@@ -355,7 +365,11 @@ const onCallDevice = async (
355
365
  await context.waitForCallbackTasks(method.connectId);
356
366
  }
357
367
 
358
- await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
368
+ await waitForPendingPromise(
369
+ method.connectId ?? '',
370
+ getPrePendingCallPromise,
371
+ removePrePendingCallPromise
372
+ );
359
373
 
360
374
  const task = requestQueue.createTask(method);
361
375
 
@@ -454,7 +468,11 @@ const onCallDevice = async (
454
468
  await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
455
469
  }
456
470
 
457
- await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
471
+ await waitForPendingPromise(
472
+ method.connectId ?? '',
473
+ getPrePendingCallPromise,
474
+ removePrePendingCallPromise
475
+ );
458
476
 
459
477
  const inner = async (): Promise<void> => {
460
478
  // Protocol is established from an active device response during acquire/initialize.
@@ -906,6 +924,16 @@ function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown)
906
924
  );
907
925
  }
908
926
 
927
+ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unknown) {
928
+ const typedError = error as { errorCode?: unknown; message?: unknown };
929
+ return (
930
+ method.payload.connectProtocol === 'V2' &&
931
+ typedError?.errorCode === HardwareErrorCode.RuntimeError &&
932
+ typeof typedError.message === 'string' &&
933
+ typedError.message.includes('Device protocol has not been detected')
934
+ );
935
+ }
936
+
909
937
  /**
910
938
  * If the Bluetooth connection times out, retry up to 6 times
911
939
  * @param retryCount - Current retry count (default 0)
@@ -943,12 +971,13 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
943
971
  DevicePool.emitter.emit(DEVICE.CONNECT, device);
944
972
  }
945
973
  } catch (err) {
974
+ const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
946
975
  // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
947
976
  // failures, so with keep-alive a wedged link would be reused by every retry
948
977
  // (field case: Initialize timing out at 25s per attempt, forever). Drop it
949
978
  // here so the next retry cold-connects.
950
979
  if (
951
- ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) &&
980
+ (ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) || requiresColdReconnect) &&
952
981
  device.mainId &&
953
982
  device.deviceConnector
954
983
  ) {
@@ -961,11 +990,12 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
961
990
  if (
962
991
  (err.errorCode === HardwareErrorCode.BleTimeoutError ||
963
992
  err.errorCode === HardwareErrorCode.BleConnectedError ||
964
- isRetryableBleProtocolV2ProbeError(method, err)) &&
993
+ isRetryableBleProtocolV2ProbeError(method, err) ||
994
+ requiresColdReconnect) &&
965
995
  retryCount < 6
966
996
  ) {
967
997
  const nextRetry = retryCount + 1;
968
- Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
998
+ Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
969
999
  await wait(3000);
970
1000
  await connectDeviceForBle(method, device, nextRetry);
971
1001
  } else {
@@ -1177,7 +1207,10 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1177
1207
  if (task && task.method?.device) {
1178
1208
  if (!canceledDevices.includes(task.method.device)) {
1179
1209
  const { device } = task.method;
1180
- setPrePendingCallPromise(device?.interruptionFromUser());
1210
+ setPrePendingCallPromise(
1211
+ task.method.connectId ?? connectId,
1212
+ device.interruptionFromUser()
1213
+ );
1181
1214
  canceledDevices.push(device);
1182
1215
  }
1183
1216
  requestQueue.rejectRequest(
@@ -1531,7 +1564,7 @@ export default class Core extends EventEmitter {
1531
1564
  private disposePromise?: Promise<void>;
1532
1565
 
1533
1566
  // background task
1534
- private prePendingCallPromise: Promise<void> | undefined;
1567
+ private prePendingCallPromises = new Map<string, Promise<void>>();
1535
1568
 
1536
1569
  private methodSynchronize = getSynchronize();
1537
1570
 
@@ -1548,9 +1581,18 @@ export default class Core extends EventEmitter {
1548
1581
  tracingContext: this.tracingContext,
1549
1582
  requestQueue: this.requestQueue,
1550
1583
  methodSynchronize: this.methodSynchronize,
1551
- getPrePendingCallPromise: () => this.prePendingCallPromise,
1552
- setPrePendingCallPromise: (promise: Promise<void> | undefined) => {
1553
- this.prePendingCallPromise = promise;
1584
+ getPrePendingCallPromise: (connectId: string) => this.prePendingCallPromises.get(connectId),
1585
+ setPrePendingCallPromise: (connectId: string, promise?: Promise<void>) => {
1586
+ if (!promise) {
1587
+ this.prePendingCallPromises.delete(connectId);
1588
+ return;
1589
+ }
1590
+ this.prePendingCallPromises.set(connectId, promise);
1591
+ },
1592
+ removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
1593
+ if (this.prePendingCallPromises.get(connectId) === promise) {
1594
+ this.prePendingCallPromises.delete(connectId);
1595
+ }
1554
1596
  },
1555
1597
  // callback 任务管理
1556
1598
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
@@ -1665,7 +1707,7 @@ export default class Core extends EventEmitter {
1665
1707
  preWarmInflight.clear();
1666
1708
  preWarmDoneAt.clear();
1667
1709
  preConnectCache = { passphraseState: undefined };
1668
- this.prePendingCallPromise = undefined;
1710
+ this.prePendingCallPromises.clear();
1669
1711
  this.removeAllListeners();
1670
1712
  cleanupSdkInstance(this.sdkInstanceId);
1671
1713
  if (_core === this) _core = undefined;
@@ -284,6 +284,9 @@ export class Device extends EventEmitter {
284
284
 
285
285
  runPromise?: Deferred<void> | null;
286
286
 
287
+ /** Resolves only after the active run has completed its release path. */
288
+ private runCleanupPromise?: Promise<void>;
289
+
287
290
  externalState: string[] = [];
288
291
 
289
292
  unavailableCapabilities: UnavailableCapabilities = {};
@@ -1107,6 +1110,10 @@ export class Device extends EventEmitter {
1107
1110
  return params.includeRaw ? cloneDeviceState(this.state) : createPublicDeviceState(this.state);
1108
1111
  }
1109
1112
 
1113
+ async refreshProtocolV2SettingsAfterMutation() {
1114
+ return this.getDeviceState({ refreshSections: ['status', 'settings'] });
1115
+ }
1116
+
1110
1117
  _updateFeatures(protoFeatures: PROTO.Features | Features, initSession?: boolean) {
1111
1118
  const previousDeviceId = this.getCurrentDeviceId();
1112
1119
  let feat =
@@ -1424,12 +1431,20 @@ export class Device extends EventEmitter {
1424
1431
 
1425
1432
  const runPromise = createDeferred<void>();
1426
1433
  this.runPromise = runPromise;
1427
- this._runInner(fn, options, runPromise).catch(error => {
1434
+ const cleanupPromise = this._runInner(fn, options, runPromise).catch(error => {
1428
1435
  if (this.runPromise === runPromise) {
1429
1436
  this.runPromise = null;
1430
1437
  }
1431
1438
  runPromise.reject(error);
1432
1439
  });
1440
+ this.runCleanupPromise = cleanupPromise;
1441
+ cleanupPromise
1442
+ .finally(() => {
1443
+ if (this.runCleanupPromise === cleanupPromise) {
1444
+ this.runCleanupPromise = undefined;
1445
+ }
1446
+ })
1447
+ .catch(() => undefined);
1433
1448
  return runPromise.promise;
1434
1449
  }
1435
1450
 
@@ -1541,13 +1556,26 @@ export class Device extends EventEmitter {
1541
1556
 
1542
1557
  async interruptionFromUser() {
1543
1558
  const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1544
- await this.cancelableAction?.(error);
1559
+ const cleanupPromise = this.runCleanupPromise;
1560
+ const { cancelableAction } = this;
1561
+ if (cancelableAction) {
1562
+ await cancelableAction(error);
1563
+ } else if (
1564
+ this.isProtocolV2() &&
1565
+ DataManager.isBleConnect(DataManager.getSettings('env')) &&
1566
+ this.hasDeviceAcquire()
1567
+ ) {
1568
+ await this.commands?.cancelDevice?.().catch(cancelError => {
1569
+ Log.debug('Protocol V2 BLE fallback cancel error', cancelError);
1570
+ });
1571
+ }
1545
1572
  await this.commands?.cancel();
1546
1573
 
1547
1574
  if (this.runPromise) {
1548
1575
  this.runPromise.reject(error);
1549
1576
  this.runPromise = null;
1550
1577
  }
1578
+ await cleanupPromise?.catch(() => undefined);
1551
1579
  }
1552
1580
 
1553
1581
  setCancelableAction(callback: (err?: Error) => Promise<unknown>) {
@@ -264,6 +264,7 @@ export class DeviceCommands {
264
264
  }
265
265
  const activeCall = this.callPromise;
266
266
  let timer: ReturnType<typeof setTimeout> | undefined;
267
+ let timedOut = false;
267
268
  const cancellation = (async () => {
268
269
  await this.dispose(true);
269
270
  await activeCall?.catch(() => undefined);
@@ -273,11 +274,23 @@ export class DeviceCommands {
273
274
  await Promise.race([
274
275
  cancellation,
275
276
  new Promise<void>(resolve => {
276
- timer = setTimeout(resolve, 10 * 1000);
277
+ timer = setTimeout(() => {
278
+ timedOut = true;
279
+ resolve();
280
+ }, 10 * 1000);
277
281
  }),
278
282
  ]);
279
283
  } finally {
280
284
  if (timer) clearTimeout(timer);
285
+ if (
286
+ timedOut &&
287
+ this.transport.name === 'ReactNativeBleTransport' &&
288
+ this.transport.disconnect
289
+ ) {
290
+ await this.transport.disconnect(this.mainId).catch(error => {
291
+ Log.debug('BLE cancellation timeout disconnect error (ignored)', error);
292
+ });
293
+ }
281
294
  if (this.callPromise === activeCall) {
282
295
  this.callPromise = undefined;
283
296
  }