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

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.133",
3
+ "version": "1.2.0-alpha.134",
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.133",
29
- "@onekeyfe/hd-transport": "1.2.0-alpha.133",
28
+ "@onekeyfe/hd-shared": "1.2.0-alpha.134",
29
+ "@onekeyfe/hd-transport": "1.2.0-alpha.134",
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": "b6d3307b47c535c876655afd99a6681399453508"
49
+ "gitHead": "47de2b0244584f6a541a84b0f94e4676bf63221a"
50
50
  }
@@ -168,7 +168,8 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
168
168
  async run() {
169
169
  try {
170
170
  if (this.device.isProtocolV2()) {
171
- const refreshStatusAndSettings = () => this.device.refreshProtocolV2SettingsAfterMutation();
171
+ const refreshStatusAndSettings = () =>
172
+ this.device.getDeviceState({ refreshSections: ['status', 'settings'] });
172
173
  assertSettingsSupported(this.payload, DEVICE_SETTINGS_V1_ONLY_FIELDS, 'Protocol V2');
173
174
  const capabilities = getDeviceSettingsCapabilities(
174
175
  this.device.getCurrentDeviceType(),
@@ -200,7 +201,20 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
200
201
  const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
201
202
  page: DeviceSettingsPage.DevicePassphrase,
202
203
  });
203
- await refreshStatusAndSettings();
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
+ }
204
218
  return res.message;
205
219
  }
206
220
  if (requestedAirgap !== undefined) {
@@ -8,7 +8,6 @@ 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';
12
11
  import {
13
12
  PRO2_WALLPAPER_HEIGHT,
14
13
  PRO2_WALLPAPER_WIDTH,
@@ -34,7 +33,6 @@ const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
34
33
  const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
35
34
  const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
36
35
  const FILESYSTEM_DIR_MAKE_MESSAGE_TYPE = 60809;
37
- const Log = getLogger(LoggerNames.Method);
38
36
 
39
37
  function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
40
38
  if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) {
@@ -145,15 +143,6 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
145
143
  const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
146
144
  settings: { wallpaper_path: this.path },
147
145
  });
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
- }
157
146
  return {
158
147
  path: this.path,
159
148
  size: encoded.data.byteLength,
package/src/core/index.ts CHANGED
@@ -83,7 +83,6 @@ 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;
87
86
 
88
87
  // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
89
88
  // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
@@ -297,35 +296,26 @@ const handlePreWarmSignal = async (
297
296
  }
298
297
  };
299
298
 
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
+
300
306
  const waitForPendingPromise = async (
301
- connectId: string,
302
- getPrePendingCallPromise: (connectId: string) => Promise<void> | undefined,
303
- removePrePendingCallPromise?: (connectId: string, promise: Promise<void>) => void
307
+ getPrePendingCallPromise: () => Promise<void> | undefined,
308
+ removePrePendingCallPromise?: (promise: Promise<void> | undefined) => void
304
309
  ) => {
305
- const pendingPromise = getPrePendingCallPromise(connectId);
310
+ const pendingPromise = getPrePendingCallPromise();
306
311
  if (pendingPromise) {
307
312
  Log.debug('pre pending call promise before call method, wait for it');
308
- let timer: ReturnType<typeof setTimeout> | undefined;
309
- let timedOut = false;
310
313
  try {
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
- ]);
314
+ await waitWithTimeout(pendingPromise, 5 * 1000);
320
315
  } catch (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 });
316
+ // ignore timeout error
328
317
  }
318
+ removePrePendingCallPromise?.(pendingPromise);
329
319
  Log.debug('pre pending call promise before call method done');
330
320
  }
331
321
  };
@@ -337,7 +327,7 @@ const onCallDevice = async (
337
327
  ): Promise<any> => {
338
328
  let messageResponse: any;
339
329
 
340
- const { requestQueue, getPrePendingCallPromise, removePrePendingCallPromise } = context;
330
+ const { requestQueue, getPrePendingCallPromise, setPrePendingCallPromise } = context;
341
331
 
342
332
  updateMethodRequestContext(method, { status: 'running' });
343
333
 
@@ -365,11 +355,7 @@ const onCallDevice = async (
365
355
  await context.waitForCallbackTasks(method.connectId);
366
356
  }
367
357
 
368
- await waitForPendingPromise(
369
- method.connectId ?? '',
370
- getPrePendingCallPromise,
371
- removePrePendingCallPromise
372
- );
358
+ await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
373
359
 
374
360
  const task = requestQueue.createTask(method);
375
361
 
@@ -468,11 +454,7 @@ const onCallDevice = async (
468
454
  await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
469
455
  }
470
456
 
471
- await waitForPendingPromise(
472
- method.connectId ?? '',
473
- getPrePendingCallPromise,
474
- removePrePendingCallPromise
475
- );
457
+ await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
476
458
 
477
459
  const inner = async (): Promise<void> => {
478
460
  // Protocol is established from an active device response during acquire/initialize.
@@ -924,16 +906,6 @@ function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown)
924
906
  );
925
907
  }
926
908
 
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
-
937
909
  /**
938
910
  * If the Bluetooth connection times out, retry up to 6 times
939
911
  * @param retryCount - Current retry count (default 0)
@@ -971,13 +943,12 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
971
943
  DevicePool.emitter.emit(DEVICE.CONNECT, device);
972
944
  }
973
945
  } catch (err) {
974
- const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
975
946
  // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
976
947
  // failures, so with keep-alive a wedged link would be reused by every retry
977
948
  // (field case: Initialize timing out at 25s per attempt, forever). Drop it
978
949
  // here so the next retry cold-connects.
979
950
  if (
980
- (ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) || requiresColdReconnect) &&
951
+ ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) &&
981
952
  device.mainId &&
982
953
  device.deviceConnector
983
954
  ) {
@@ -990,12 +961,11 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
990
961
  if (
991
962
  (err.errorCode === HardwareErrorCode.BleTimeoutError ||
992
963
  err.errorCode === HardwareErrorCode.BleConnectedError ||
993
- isRetryableBleProtocolV2ProbeError(method, err) ||
994
- requiresColdReconnect) &&
964
+ isRetryableBleProtocolV2ProbeError(method, err)) &&
995
965
  retryCount < 6
996
966
  ) {
997
967
  const nextRetry = retryCount + 1;
998
- Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
968
+ Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
999
969
  await wait(3000);
1000
970
  await connectDeviceForBle(method, device, nextRetry);
1001
971
  } else {
@@ -1207,10 +1177,7 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1207
1177
  if (task && task.method?.device) {
1208
1178
  if (!canceledDevices.includes(task.method.device)) {
1209
1179
  const { device } = task.method;
1210
- setPrePendingCallPromise(
1211
- task.method.connectId ?? connectId,
1212
- device.interruptionFromUser()
1213
- );
1180
+ setPrePendingCallPromise(device?.interruptionFromUser());
1214
1181
  canceledDevices.push(device);
1215
1182
  }
1216
1183
  requestQueue.rejectRequest(
@@ -1564,7 +1531,7 @@ export default class Core extends EventEmitter {
1564
1531
  private disposePromise?: Promise<void>;
1565
1532
 
1566
1533
  // background task
1567
- private prePendingCallPromises = new Map<string, Promise<void>>();
1534
+ private prePendingCallPromise: Promise<void> | undefined;
1568
1535
 
1569
1536
  private methodSynchronize = getSynchronize();
1570
1537
 
@@ -1581,18 +1548,9 @@ export default class Core extends EventEmitter {
1581
1548
  tracingContext: this.tracingContext,
1582
1549
  requestQueue: this.requestQueue,
1583
1550
  methodSynchronize: this.methodSynchronize,
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
- }
1551
+ getPrePendingCallPromise: () => this.prePendingCallPromise,
1552
+ setPrePendingCallPromise: (promise: Promise<void> | undefined) => {
1553
+ this.prePendingCallPromise = promise;
1596
1554
  },
1597
1555
  // callback 任务管理
1598
1556
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
@@ -1707,7 +1665,7 @@ export default class Core extends EventEmitter {
1707
1665
  preWarmInflight.clear();
1708
1666
  preWarmDoneAt.clear();
1709
1667
  preConnectCache = { passphraseState: undefined };
1710
- this.prePendingCallPromises.clear();
1668
+ this.prePendingCallPromise = undefined;
1711
1669
  this.removeAllListeners();
1712
1670
  cleanupSdkInstance(this.sdkInstanceId);
1713
1671
  if (_core === this) _core = undefined;
@@ -284,9 +284,6 @@ 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
-
290
287
  externalState: string[] = [];
291
288
 
292
289
  unavailableCapabilities: UnavailableCapabilities = {};
@@ -930,17 +927,6 @@ export class Device extends EventEmitter {
930
927
  };
931
928
 
932
929
  const expectedDeviceId = options?.deviceId;
933
- if (expectedDeviceId) {
934
- // 先只读校验物理设备身份;钱包上下文仍由下方携带完整参数的
935
- // Initialize 选择,避免标准钱包请求复用此前的隐藏钱包上下文。
936
- this.passphraseState = undefined;
937
- const { message } = await this.commands.typedCall('GetFeatures', 'Features', {});
938
- this._updateFeatures(message);
939
- await TransportManager.reconfigure(this.features);
940
- if (!this.checkDeviceId(expectedDeviceId)) {
941
- throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
942
- }
943
- }
944
930
 
945
931
  this.passphraseState = options?.passphraseState;
946
932
 
@@ -960,7 +946,46 @@ export class Device extends EventEmitter {
960
946
  payload.derive_cardano = true;
961
947
  }
962
948
 
963
- await callInitialize(payload, options?.initSession);
949
+ if (this.features) {
950
+ // Re-sync the V1 message schema for THIS device before encoding
951
+ // Initialize: the process-global schema may still reflect another
952
+ // device (e.g. legacy-firmware Touch/Mini) on multi-device setups, and
953
+ // a stale legacy schema would silently strip passphrase_state /
954
+ // is_contains_attach from the wire message. Local operation, no wire
955
+ // I/O; a no-op when the schema is unchanged.
956
+ await TransportManager.reconfigure(this.features);
957
+ }
958
+
959
+ // Initialize's own Features response carries device_id, so the physical
960
+ // device identity is validated on the same round trip instead of via a
961
+ // separate read-only GetFeatures preflight (which doubled the wire cost
962
+ // of every deviceId-carrying call). The method fn has not run yet, so a
963
+ // mismatch still fails before any wallet data can be derived, with the
964
+ // same DeviceCheckDeviceIdError. Wallet-context selection is unchanged:
965
+ // it is decided by the Initialize payload above either way.
966
+ const assertExpectedDeviceIdentity = () => {
967
+ if (expectedDeviceId && !this.checkDeviceId(expectedDeviceId)) {
968
+ // The mismatched Initialize may have cached a session under the wrong
969
+ // device's identity; drop it so no wallet context survives from it.
970
+ // (This also evicts any session the wrong device legitimately cached
971
+ // under the same passphraseState — a deliberate conservative purge
972
+ // after a physical-swap event, consistent with
973
+ // reconcileDeviceIdentity purging the previous device's sessions.)
974
+ this.clearInternalState();
975
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
976
+ }
977
+ };
978
+
979
+ try {
980
+ await callInitialize(payload, options?.initSession);
981
+ } catch (error) {
982
+ // callInitialize can fail AFTER the wire call cached the session (e.g.
983
+ // TransportManager.reconfigure rejecting); the identity check must
984
+ // still run so a wrong-device session never survives the error path.
985
+ assertExpectedDeviceIdentity();
986
+ throw error;
987
+ }
988
+ assertExpectedDeviceIdentity();
964
989
  } catch (error) {
965
990
  Log.error('Initialization failed:', error);
966
991
  throw error;
@@ -1082,10 +1107,6 @@ export class Device extends EventEmitter {
1082
1107
  return params.includeRaw ? cloneDeviceState(this.state) : createPublicDeviceState(this.state);
1083
1108
  }
1084
1109
 
1085
- async refreshProtocolV2SettingsAfterMutation() {
1086
- return this.getDeviceState({ refreshSections: ['status', 'settings'] });
1087
- }
1088
-
1089
1110
  _updateFeatures(protoFeatures: PROTO.Features | Features, initSession?: boolean) {
1090
1111
  const previousDeviceId = this.getCurrentDeviceId();
1091
1112
  let feat =
@@ -1403,20 +1424,12 @@ export class Device extends EventEmitter {
1403
1424
 
1404
1425
  const runPromise = createDeferred<void>();
1405
1426
  this.runPromise = runPromise;
1406
- const cleanupPromise = this._runInner(fn, options, runPromise).catch(error => {
1427
+ this._runInner(fn, options, runPromise).catch(error => {
1407
1428
  if (this.runPromise === runPromise) {
1408
1429
  this.runPromise = null;
1409
1430
  }
1410
1431
  runPromise.reject(error);
1411
1432
  });
1412
- this.runCleanupPromise = cleanupPromise;
1413
- cleanupPromise
1414
- .finally(() => {
1415
- if (this.runCleanupPromise === cleanupPromise) {
1416
- this.runCleanupPromise = undefined;
1417
- }
1418
- })
1419
- .catch(() => undefined);
1420
1433
  return runPromise.promise;
1421
1434
  }
1422
1435
 
@@ -1528,26 +1541,13 @@ export class Device extends EventEmitter {
1528
1541
 
1529
1542
  async interruptionFromUser() {
1530
1543
  const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1531
- const cleanupPromise = this.runCleanupPromise;
1532
- const { cancelableAction } = this;
1533
- if (cancelableAction) {
1534
- await cancelableAction(error);
1535
- } else if (
1536
- this.isProtocolV2() &&
1537
- DataManager.isBleConnect(DataManager.getSettings('env')) &&
1538
- this.hasDeviceAcquire()
1539
- ) {
1540
- await this.commands?.cancelDevice?.().catch(cancelError => {
1541
- Log.debug('Protocol V2 BLE fallback cancel error', cancelError);
1542
- });
1543
- }
1544
+ await this.cancelableAction?.(error);
1544
1545
  await this.commands?.cancel();
1545
1546
 
1546
1547
  if (this.runPromise) {
1547
1548
  this.runPromise.reject(error);
1548
1549
  this.runPromise = null;
1549
1550
  }
1550
- await cleanupPromise?.catch(() => undefined);
1551
1551
  }
1552
1552
 
1553
1553
  setCancelableAction(callback: (err?: Error) => Promise<unknown>) {
@@ -264,7 +264,6 @@ export class DeviceCommands {
264
264
  }
265
265
  const activeCall = this.callPromise;
266
266
  let timer: ReturnType<typeof setTimeout> | undefined;
267
- let timedOut = false;
268
267
  const cancellation = (async () => {
269
268
  await this.dispose(true);
270
269
  await activeCall?.catch(() => undefined);
@@ -274,23 +273,11 @@ export class DeviceCommands {
274
273
  await Promise.race([
275
274
  cancellation,
276
275
  new Promise<void>(resolve => {
277
- timer = setTimeout(() => {
278
- timedOut = true;
279
- resolve();
280
- }, 10 * 1000);
276
+ timer = setTimeout(resolve, 10 * 1000);
281
277
  }),
282
278
  ]);
283
279
  } finally {
284
280
  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
- }
294
281
  if (this.callPromise === activeCall) {
295
282
  this.callPromise = undefined;
296
283
  }