@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.12

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 (79) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +13 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  4. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
  5. package/__tests__/device-lifecycle-events.test.ts +86 -0
  6. package/__tests__/deviceUploadNft.test.ts +68 -0
  7. package/__tests__/evmSignTypedData.test.ts +42 -0
  8. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
  9. package/__tests__/logBlockEvent.test.ts +45 -122
  10. package/__tests__/open-wallet-session.test.ts +781 -69
  11. package/__tests__/pro2HostAssetPackage.test.ts +165 -0
  12. package/__tests__/protocol-v2-resources.test.ts +7 -4
  13. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  14. package/__tests__/protocol-v2.test.ts +925 -97
  15. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  16. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  18. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  20. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  22. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +2 -2
  24. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  25. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  26. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  28. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  30. package/dist/core/RequestQueue.d.ts +2 -0
  31. package/dist/core/RequestQueue.d.ts.map +1 -1
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/device/Device.d.ts +1 -1
  35. package/dist/device/Device.d.ts.map +1 -1
  36. package/dist/device/DeviceConnector.d.ts +1 -1
  37. package/dist/device/DeviceConnector.d.ts.map +1 -1
  38. package/dist/events/logBlockEvent.d.ts.map +1 -1
  39. package/dist/events/ui-request.d.ts +3 -0
  40. package/dist/events/ui-request.d.ts.map +1 -1
  41. package/dist/index.d.ts +21 -5
  42. package/dist/index.js +2511 -1538
  43. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/settings.d.ts +2 -2
  49. package/dist/types/settings.d.ts.map +1 -1
  50. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  51. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  52. package/dist/utils/patch.d.ts +1 -1
  53. package/dist/utils/patch.d.ts.map +1 -1
  54. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  55. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  56. package/package.json +4 -4
  57. package/src/api/FirmwareUpdateV4.ts +474 -83
  58. package/src/api/OpenWalletSession.ts +39 -12
  59. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  60. package/src/api/evm/EVMSignTypedData.ts +1 -0
  61. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  62. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  63. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  64. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  65. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
  66. package/src/api/protocol-v2/helpers.ts +8 -0
  67. package/src/core/RequestQueue.ts +16 -1
  68. package/src/core/index.ts +164 -26
  69. package/src/data/messages/messages-protocol-v2.json +1589 -1375
  70. package/src/device/Device.ts +3 -1
  71. package/src/device/DeviceConnector.ts +7 -3
  72. package/src/events/logBlockEvent.ts +6 -75
  73. package/src/events/ui-request.ts +3 -0
  74. package/src/protocols/protocol-v2/resources.ts +9 -1
  75. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  76. package/src/protocols/protocol-v2/walletSession.ts +223 -56
  77. package/src/types/settings.ts +4 -2
  78. package/src/utils/deviceFeaturesUtils.ts +4 -0
  79. package/src/utils/pro2HostAssetPackage.ts +400 -0
package/src/core/index.ts CHANGED
@@ -2,7 +2,11 @@ import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
3
  import {
4
4
  DeviceSessionPinType,
5
+ type LowlevelTransportSharedPlugin,
6
+ type OneKeyDeviceInfo,
7
+ type ProtocolType,
5
8
  TRANSPORT_EVENT,
9
+ type TransportDeviceDisconnectEvent,
6
10
  isProtocolV2LinkDisabledError,
7
11
  } from '@onekeyfe/hd-transport';
8
12
  import {
@@ -74,11 +78,6 @@ import type { CoreMessage, IFrameCallMessage, UiPromise, UiPromiseResponse } fro
74
78
  import type { DeviceEvents, InitOptions, RunOptions } from '../device/Device';
75
79
  import type { SdkTracingContext } from '../utils/tracing';
76
80
  import type { Deferred } from '@onekeyfe/hd-shared';
77
- import type {
78
- LowlevelTransportSharedPlugin,
79
- OneKeyDeviceInfo,
80
- TransportDeviceDisconnectEvent,
81
- } from '@onekeyfe/hd-transport';
82
81
  import type { BaseMethod } from '../api/BaseMethod';
83
82
 
84
83
  const Log = getLogger(LoggerNames.Core);
@@ -92,7 +91,7 @@ const preWarmDoneAt = new Map<string, number>();
92
91
 
93
92
  export type CoreContext = ReturnType<Core['getCoreContext']>;
94
93
 
95
- function hasDeriveCardano(method: BaseMethod): boolean {
94
+ function resolveDeriveCardano(method: BaseMethod): boolean | undefined {
96
95
  if (
97
96
  method.name.startsWith('allNetworkGetAddress') &&
98
97
  method.payload &&
@@ -102,15 +101,22 @@ function hasDeriveCardano(method: BaseMethod): boolean {
102
101
  ) {
103
102
  return true;
104
103
  }
105
-
106
- return method.name.startsWith('cardano') || method.payload?.deriveCardano;
104
+ if (method.name.startsWith('cardano')) {
105
+ return true;
106
+ }
107
+ // V1 Initialize only sends derive_cardano on an explicit true.
108
+ // V2 AskPassphrase maps true to [Standard, Cardano] and anything else to [Standard].
109
+ if (method.payload?.deriveCardano === true) {
110
+ return true;
111
+ }
112
+ return undefined;
107
113
  }
108
114
 
109
115
  const parseInitOptions = (method?: BaseMethod): InitOptions => ({
110
116
  initSession: method?.payload.initSession,
111
117
  passphraseState: method?.payload.useEmptyPassphrase ? undefined : method?.payload.passphraseState,
112
118
  deviceId: method?.payload.deviceId,
113
- deriveCardano: method && hasDeriveCardano(method),
119
+ deriveCardano: method ? resolveDeriveCardano(method) : undefined,
114
120
  connectProtocol: method?.payload.connectProtocol,
115
121
  forceProtocolDetection: method?.payload.forceProtocolDetection,
116
122
  protocolV2DeviceInfoTimeoutMs: method?.payload.protocolV2DeviceInfoTimeoutMs,
@@ -415,6 +421,13 @@ const onCallDevice = async (
415
421
  if (method.payload?.onlyConnectBleDevice) {
416
422
  preWarmCallbackTask?.resolve();
417
423
  Log.debug('Call API - only connect ble device: ', device?.mainId);
424
+ // This early return bypasses the normal-path bookkeeping at the end of the
425
+ // call. Without it the task leaks and haunts every later queue snapshot
426
+ // and cancel sweep (field log: a completed task lingered for 6 minutes),
427
+ // and the request stays in the active maps, so repeated preconnects pile
428
+ // up phantom work in diagnostics.
429
+ completeMethodRequestContext(method);
430
+ requestQueue.releaseTask(method.responseID);
418
431
  return createResponseMessage(method.responseID, true, null);
419
432
  }
420
433
 
@@ -644,7 +657,8 @@ const onCallDevice = async (
644
657
  method.payload?.passphraseState,
645
658
  method.payload?.useEmptyPassphrase,
646
659
  method.payload?.skipPassphraseCheck,
647
- hasDeriveCardano(method)
660
+ resolveDeriveCardano(method),
661
+ method.protocolV2UnlockContext?.preflightMainPinSelected
648
662
  );
649
663
 
650
664
  // Double check, handles the special case of Touch/Pro
@@ -950,11 +964,72 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
950
964
  );
951
965
  }
952
966
 
967
+ export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
968
+ return (
969
+ method.payload.connectProtocol === 'V2' &&
970
+ (error as { errorCode?: unknown })?.errorCode ===
971
+ HardwareErrorCode.BlePeerRemovedPairingInformation
972
+ );
973
+ }
974
+
953
975
  /**
954
976
  * If the Bluetooth connection times out, retry up to 6 times
955
977
  * @param retryCount - Current retry count (default 0)
956
978
  */
957
- async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
979
+ // device.acquire awaits a transport reply with no deadline of its own; a
980
+ // transport that never settles (field case: Electron main lost an IPC reply,
981
+ // "reply was never sent" after 5 minutes) hangs the call forever and cancel()
982
+ // only takes effect at poll checkpoints. Race acquire against a deadline and
983
+ // the caller's abort signal so the hang is bounded and cancel is immediate.
984
+ const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
985
+
986
+ function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
987
+ return new Promise<T>((resolve, reject) => {
988
+ let settled = false;
989
+ const settle = (fn: () => void) => {
990
+ if (settled) return;
991
+ settled = true;
992
+ clearTimeout(deadline);
993
+ abortSignal?.removeEventListener('abort', onAbort);
994
+ fn();
995
+ };
996
+ const onAbort = () =>
997
+ settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
998
+ const deadline = setTimeout(
999
+ () =>
1000
+ settle(() =>
1001
+ reject(
1002
+ ERRORS.TypedError(
1003
+ HardwareErrorCode.BleTimeoutError,
1004
+ `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
1005
+ )
1006
+ )
1007
+ ),
1008
+ BLE_ACQUIRE_DEADLINE_MS
1009
+ );
1010
+ // Attach before any early return so a late settlement of acquirePromise
1011
+ // is always consumed — an abort or deadline must never leave the acquire
1012
+ // rejection unhandled.
1013
+ acquirePromise.then(
1014
+ value => settle(() => resolve(value)),
1015
+ error => settle(() => reject(error))
1016
+ );
1017
+ if (abortSignal) {
1018
+ if (abortSignal.aborted) {
1019
+ onAbort();
1020
+ return;
1021
+ }
1022
+ abortSignal.addEventListener('abort', onAbort);
1023
+ }
1024
+ });
1025
+ }
1026
+
1027
+ async function connectDeviceForBle(
1028
+ method: BaseMethod,
1029
+ device: Device,
1030
+ abortSignal?: AbortSignal,
1031
+ retryCount = 0
1032
+ ) {
958
1033
  try {
959
1034
  if (device.wasInterruptedByUser()) {
960
1035
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
@@ -968,9 +1043,44 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
968
1043
  !device.commands ||
969
1044
  device.commands.disposed;
970
1045
  if (shouldAcquire) {
971
- await device.acquire(method.payload.connectProtocol, {
972
- forceProtocolDetection: method.payload.forceProtocolDetection,
973
- });
1046
+ const connectProtocol = resolveBleConnectProtocol(method);
1047
+ // The deadline/abort guards are scoped to the desktop electron
1048
+ // transport: its IPC acquire is the only path with a proven
1049
+ // never-settling failure mode, while react-native/lowlevel acquire may
1050
+ // legitimately block on a user-driven system bonding prompt for longer
1051
+ // than any sane deadline. Other envs keep the plain acquire unchanged.
1052
+ const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
1053
+ // A cancel landing during the retry backoff must not start a new acquire.
1054
+ if (useAcquireGuards && abortSignal?.aborted) {
1055
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1056
+ }
1057
+ if (!useAcquireGuards) {
1058
+ await device.acquire(connectProtocol, {
1059
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1060
+ });
1061
+ } else {
1062
+ try {
1063
+ await raceBleAcquire(
1064
+ device.acquire(connectProtocol, {
1065
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1066
+ }),
1067
+ abortSignal
1068
+ );
1069
+ } catch (err) {
1070
+ // A deadline hit means the transport is wedged mid-acquire; drop the
1071
+ // link before the retry so it cold-connects instead of stacking a
1072
+ // second connect onto the half-open one.
1073
+ if (
1074
+ err.errorCode === HardwareErrorCode.BleTimeoutError &&
1075
+ device.mainId &&
1076
+ device.deviceConnector
1077
+ ) {
1078
+ await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
1079
+ device.markTransportDisconnected();
1080
+ }
1081
+ throw err;
1082
+ }
1083
+ }
974
1084
  }
975
1085
  if (method.payload?.onlyConnectBleDevice) {
976
1086
  if (shouldAcquire) {
@@ -1010,13 +1120,21 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
1010
1120
  const nextRetry = retryCount + 1;
1011
1121
  Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
1012
1122
  await wait(3000);
1013
- await connectDeviceForBle(method, device, nextRetry);
1123
+ await connectDeviceForBle(method, device, abortSignal, nextRetry);
1014
1124
  } else {
1015
1125
  throw err;
1016
1126
  }
1017
1127
  }
1018
1128
  }
1019
1129
 
1130
+ export function resolveBleConnectProtocol(method: BaseMethod): ProtocolType | undefined {
1131
+ if (method.payload.connectProtocol === 'V1' || method.payload.connectProtocol === 'V2') {
1132
+ return method.payload.connectProtocol;
1133
+ }
1134
+ const supportedProtocols = method.getSupportedProtocols();
1135
+ return supportedProtocols.length === 1 && supportedProtocols[0] === 'V2' ? 'V2' : undefined;
1136
+ }
1137
+
1020
1138
  type IPollFn<T> = (time?: number) => T;
1021
1139
  // eslint-disable-next-line @typescript-eslint/require-await
1022
1140
  const ensureConnected = async (
@@ -1120,7 +1238,7 @@ const ensureConnected = async (
1120
1238
  if (tryCount === 1) {
1121
1239
  device.beginConnectionAttempt();
1122
1240
  }
1123
- await connectDeviceForBle(method, device);
1241
+ await connectDeviceForBle(method, device, abortSignal);
1124
1242
  }
1125
1243
  resolve(device);
1126
1244
  return;
@@ -1164,7 +1282,8 @@ const ensureConnected = async (
1164
1282
  HardwareErrorCode.BridgeNeedsPermission,
1165
1283
  HardwareErrorCode.DeviceInterruptedFromUser,
1166
1284
  HardwareErrorCode.CallQueueActionCancelled,
1167
- ].includes(error.errorCode)
1285
+ ].includes(error.errorCode) ||
1286
+ isProtocolV2PeerRemovedPairingError(method, error)
1168
1287
  ) {
1169
1288
  reject(error);
1170
1289
  return;
@@ -1214,12 +1333,17 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1214
1333
  // cancel callback tasks
1215
1334
  requestQueue.cancelCallbackTasks(connectId);
1216
1335
 
1217
- const requestIds = requestQueue.getRequestTasksId();
1336
+ const requestIds = requestQueue.getRequestTasksIdByConnectId(connectId);
1218
1337
  Log.debug(
1219
1338
  `Cancel Api connect requestQueues: length:${requestIds.length} requestIds:${requestIds.join(
1220
1339
  ','
1221
1340
  )}`
1222
1341
  );
1342
+ // Abort before rejecting: rejectRequest releases the task and would make
1343
+ // its AbortController unreachable to an in-flight method loop.
1344
+ // Match both the requested connectId and a device selected internally by the
1345
+ // method, such as Desktop WebUSB firmwareUpdateV4.
1346
+ requestQueue.abortRequestsByConnectId(connectId);
1223
1347
  const canceledDevices: Device[] = [];
1224
1348
  const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
1225
1349
  if (!device || canceledDevices.includes(device)) {
@@ -1235,7 +1359,7 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1235
1359
  // During ensureConnected the method has a connectId but device is
1236
1360
  // assigned only after the poll succeeds. Interrupt the cached BLE
1237
1361
  // Device so an in-flight acquire/initialize cannot finish.
1238
- interruptDevice(task.method?.device, task.method.connectId ?? connectId);
1362
+ interruptDevice(task.method?.device, connectId);
1239
1363
  interruptDevice(deviceCacheMap.get(connectId), connectId);
1240
1364
  requestQueue.rejectRequest(
1241
1365
  requestId,
@@ -1244,13 +1368,15 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1244
1368
  }
1245
1369
  }
1246
1370
  interruptDevice(deviceCacheMap.get(connectId), connectId);
1247
- requestQueue.abortRequestsByConnectId(connectId);
1248
1371
  pollingManager.stop(connectId);
1249
1372
  } catch (e) {
1250
1373
  Log.error('Cancel API Error: ', e);
1251
1374
  }
1252
1375
  } else {
1253
1376
  const env = DataManager.getSettings('env');
1377
+ // Abort every method before rejecting its queue task. Non-BLE methods also
1378
+ // use the signal to stop recovery loops after the public promise is rejected.
1379
+ requestQueue.abortAllRequests();
1254
1380
  if (DataManager.isBleConnect(env)) {
1255
1381
  Log.debug('Cancel Api all _deviceList: ');
1256
1382
  const canceledDevices: Device[] = [];
@@ -1277,7 +1403,6 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1277
1403
  }
1278
1404
  }
1279
1405
  deviceCacheMap.forEach(interruptDevice);
1280
- requestQueue.abortAllRequests();
1281
1406
  pollingManager.stopAll();
1282
1407
  } else {
1283
1408
  _deviceList?.allDevices().forEach(device => {
@@ -1296,8 +1421,10 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1296
1421
  }
1297
1422
  }
1298
1423
 
1299
- cleanup();
1300
- closePopup();
1424
+ cleanup(connectId);
1425
+ if (!connectId || _uiPromises.length === 0) {
1426
+ closePopup();
1427
+ }
1301
1428
  };
1302
1429
 
1303
1430
  const checkPassphraseEnableState = (method: BaseMethod, features?: Features) => {
@@ -1328,12 +1455,23 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) =>
1328
1455
  const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
1329
1456
  if (!method.useDevicePassphraseState) return false;
1330
1457
 
1458
+ if (device.isProtocolV2() && method.payload?.useEmptyPassphrase === true) {
1459
+ return true;
1460
+ }
1461
+
1331
1462
  return device.hasUsePassphrase();
1332
1463
  };
1333
1464
 
1334
- const cleanup = () => {
1335
- const pendingUiPromises = _uiPromises;
1336
- _uiPromises = [];
1465
+ const cleanup = (connectId?: string) => {
1466
+ const pendingUiPromises = connectId
1467
+ ? _uiPromises.filter(
1468
+ uiPromise =>
1469
+ uiPromise.data?.mainId === connectId || uiPromise.data?.getConnectId() === connectId
1470
+ )
1471
+ : _uiPromises;
1472
+ _uiPromises = connectId
1473
+ ? _uiPromises.filter(uiPromise => !pendingUiPromises.includes(uiPromise))
1474
+ : [];
1337
1475
  rejectUiPromises(
1338
1476
  pendingUiPromises,
1339
1477
  ERRORS.TypedError(HardwareErrorCode.ActionCancelled, 'UI request was cancelled')