@onekeyfe/hd-transport-react-native 1.2.2-alpha.114 → 1.2.2-alpha.115

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/dist/index.d.ts CHANGED
@@ -135,6 +135,15 @@ declare class ReactNativeBleTransport {
135
135
  private confirmedProtocolV2;
136
136
  /** Consecutive detections that failed while trusting sessionProtocols. */
137
137
  private protocolReprobeFailures;
138
+ /** Endpoints whose last detection got no answer on any protocol. */
139
+ private silentDetections;
140
+ /**
141
+ * Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
142
+ * on the device, and a failed detection tears the link down and reconnects on every
143
+ * poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
144
+ * detection succeeds, which re-arms it for the next time the device sleeps.
145
+ */
146
+ private protocolWakeAttempts;
138
147
  /**
139
148
  * Native encryption/pairing failures seen before Protocol V2 probe starts.
140
149
  * Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
@@ -228,6 +237,23 @@ declare class ReactNativeBleTransport {
228
237
  /** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
229
238
  private getActiveProtocol;
230
239
  private detectProtocol;
240
+ /**
241
+ * Sent once before probing when the previous detection on this endpoint got no answer
242
+ * at all. On Classic-family firmware that silence is also what a sleeping device looks
243
+ * like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
244
+ * sits in the screensaver loop, whose host-message filter accepts only Initialize and
245
+ * the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
246
+ * amount of probing can bring the device back.
247
+ *
248
+ * Initialize is the one message that breaks that loop, but it also starts a fresh
249
+ * wallet session, which is why it is not the probe itself — a bare Initialize on every
250
+ * acquire would drop a hidden-wallet session before Core can restore it. Requiring a
251
+ * fully silent previous detection keeps it to devices with no session left to protect.
252
+ *
253
+ * The firmware consumes the wake to leave its loop and does not reliably answer it, so
254
+ * the result is ignored: the probes that follow are what decide the protocol.
255
+ */
256
+ private wakeSilentProtocolV1Device;
231
257
  private resetProbeStateAfterProtocolProbe;
232
258
  private probeProtocolV1;
233
259
  private probeProtocolV2;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -446,10 +446,9 @@ const isWedgedBleSetupError = (error) => (error === null || error === void 0 ? v
446
446
  error.message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
447
447
  const shouldRethrowBleSetupError = (error) => isConnectTimeoutError(error) || isWedgedBleSetupError(error);
448
448
  const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
449
- const isMtuOrCancelledConnectError = (error) => {
449
+ const isAbandonedNativeOperationError = (error) => {
450
450
  const errorCode = error === null || error === void 0 ? void 0 : error.errorCode;
451
- return (errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
452
- errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled);
451
+ return (errorCode === reactNativeBlePlx.BleErrorCode.OperationTimedOut || errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled);
453
452
  };
454
453
  const tryToGetConfiguration = (device) => {
455
454
  if (!device || !device.serviceUUIDs)
@@ -464,7 +463,7 @@ const tryToGetConfiguration = (device) => {
464
463
  };
465
464
  const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __awaiter(void 0, void 0, void 0, function* () {
466
465
  if (reactNative.Platform.OS !== 'ios' && reactNative.Platform.OS !== 'android')
467
- return { device, timedOut: false };
466
+ return { device, abandoned: false };
468
467
  const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
469
468
  let timeoutId;
470
469
  let timedOut = false;
@@ -480,7 +479,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
480
479
  }, BLE_MTU_REQUEST_TIMEOUT_MS);
481
480
  }),
482
481
  ]);
483
- return { device: mtuDevice, timedOut: false };
482
+ return { device: mtuDevice, abandoned: false };
484
483
  }
485
484
  catch (error) {
486
485
  if (timedOut && cancelTransaction) {
@@ -508,10 +507,9 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
508
507
  stage,
509
508
  attempt,
510
509
  actual: device.mtu,
511
- timedOut,
512
510
  error: error instanceof Error ? error.message : String(error),
513
511
  });
514
- return { device, timedOut };
512
+ return { device, abandoned: timedOut };
515
513
  }
516
514
  finally {
517
515
  if (timeoutId)
@@ -520,7 +518,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
520
518
  });
521
519
  const resolveNegotiatedMtu = (device, cancelTransaction) => shouldRefreshNegotiatedMtu(device.mtu)
522
520
  ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
523
- : Promise.resolve({ device, timedOut: false });
521
+ : Promise.resolve({ device, abandoned: false });
524
522
  function remapError(error) {
525
523
  var _a;
526
524
  if (error instanceof reactNativeBlePlx.BleError) {
@@ -557,6 +555,8 @@ class ReactNativeBleTransport {
557
555
  this.sessionProtocols = new Map();
558
556
  this.confirmedProtocolV2 = new Set();
559
557
  this.protocolReprobeFailures = new Map();
558
+ this.silentDetections = new Set();
559
+ this.protocolWakeAttempts = new Set();
560
560
  this.staleBondErrors = new Map();
561
561
  this.acquiringProtocolV2 = new Set();
562
562
  this.protocolV2Assemblers = new Map();
@@ -1043,6 +1043,7 @@ class ReactNativeBleTransport {
1043
1043
  }
1044
1044
  }
1045
1045
  let device = null;
1046
+ let mtuHandshakeRefused = false;
1046
1047
  if (forceCleanRunPromise && this.runPromise) {
1047
1048
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
1048
1049
  this.runPromise.reject(error);
@@ -1051,7 +1052,6 @@ class ReactNativeBleTransport {
1051
1052
  Log === null || Log === void 0 ? void 0 : Log.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
1052
1053
  }
1053
1054
  const blePlxManager = yield this.getPlxManager();
1054
- let skipPostConnectMtu = false;
1055
1055
  try {
1056
1056
  yield subscribeBleOn(blePlxManager);
1057
1057
  }
@@ -1100,9 +1100,11 @@ class ReactNativeBleTransport {
1100
1100
  if (shouldRethrowBleSetupError(e)) {
1101
1101
  throw e;
1102
1102
  }
1103
- if (isMtuOrCancelledConnectError(e)) {
1104
- skipPostConnectMtu = true;
1103
+ if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
1104
+ e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
1105
1105
  Log === null || Log === void 0 ? void 0 : Log.debug('first try to reconnect without params');
1106
+ mtuHandshakeRefused = true;
1107
+ yield this.runBestEffortNativeOperation('connect cancelled: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
1106
1108
  device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, fallbackConnectOptions));
1107
1109
  }
1108
1110
  else if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceAlreadyConnected) {
@@ -1128,9 +1130,11 @@ class ReactNativeBleTransport {
1128
1130
  if (shouldRethrowBleSetupError(e)) {
1129
1131
  throw e;
1130
1132
  }
1131
- if (isMtuOrCancelledConnectError(e)) {
1132
- skipPostConnectMtu = true;
1133
+ if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
1134
+ e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
1133
1135
  Log === null || Log === void 0 ? void 0 : Log.debug('second try to reconnect without params');
1136
+ mtuHandshakeRefused = true;
1137
+ yield this.runBestEffortNativeOperation('connect cancelled: cancel device connection', () => disconnectedDevice.cancelConnection());
1134
1138
  try {
1135
1139
  device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(fallbackConnectOptions));
1136
1140
  }
@@ -1165,18 +1169,20 @@ class ReactNativeBleTransport {
1165
1169
  }
1166
1170
  if (this.stopped)
1167
1171
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1168
- if (!skipPostConnectMtu) {
1169
- const mtuResult = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
1170
- device = mtuResult.device;
1171
- if (mtuResult.timedOut) {
1172
- if (this.stopped)
1173
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1174
- Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU');
1175
- const timedOutDevice = device;
1176
- yield this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () => timedOutDevice.cancelConnection());
1177
- if (this.stopped)
1178
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1179
- device = yield this.connectWithTimeout(uuid, () => timedOutDevice.connect(fallbackConnectOptions));
1172
+ if (mtuHandshakeRefused) {
1173
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] skipping MTU refresh after cancelled MTU connect', {
1174
+ connectIdSuffix: uuid.slice(-8),
1175
+ actual: device.mtu,
1176
+ });
1177
+ }
1178
+ else {
1179
+ const negotiatedMtu = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
1180
+ device = negotiatedMtu.device;
1181
+ if (negotiatedMtu.abandoned) {
1182
+ yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
1183
+ yield this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
1184
+ }));
1185
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'BLE MTU negotiation abandoned, reconnecting');
1180
1186
  }
1181
1187
  }
1182
1188
  if (this.stopped)
@@ -1893,7 +1899,7 @@ class ReactNativeBleTransport {
1893
1899
  return result;
1894
1900
  }
1895
1901
  catch (error) {
1896
- if (timedOut || isNativeOperationTimeoutError(error)) {
1902
+ if (timedOut || isAbandonedNativeOperationError(error)) {
1897
1903
  const resetManager = this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1898
1904
  if (resetManager) {
1899
1905
  throw this.createWedgedBleSetupError();
@@ -2050,6 +2056,8 @@ class ReactNativeBleTransport {
2050
2056
  this.acquiringProtocolV2.clear();
2051
2057
  this.sessionProtocols.clear();
2052
2058
  this.protocolReprobeFailures.clear();
2059
+ this.silentDetections.clear();
2060
+ this.protocolWakeAttempts.clear();
2053
2061
  this.writeTimeoutCounts.clear();
2054
2062
  this.connectionSetupTimeoutCounts.clear();
2055
2063
  this.monitorTokens.clear();
@@ -2123,6 +2131,7 @@ class ReactNativeBleTransport {
2123
2131
  !protocolHint &&
2124
2132
  reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
2125
2133
  const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
2134
+ yield this.wakeSilentProtocolV1Device(uuid, probeOrder);
2126
2135
  for (let i = 0; i < probeOrder.length; i += 1) {
2127
2136
  const protocol = probeOrder[i];
2128
2137
  if (i > 0) {
@@ -2142,6 +2151,8 @@ class ReactNativeBleTransport {
2142
2151
  this.confirmedProtocolV2.add(uuid);
2143
2152
  }
2144
2153
  this.protocolReprobeFailures.delete(uuid);
2154
+ this.silentDetections.delete(uuid);
2155
+ this.protocolWakeAttempts.delete(uuid);
2145
2156
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
2146
2157
  deviceId: uuid,
2147
2158
  protocol,
@@ -2150,6 +2161,7 @@ class ReactNativeBleTransport {
2150
2161
  return protocol;
2151
2162
  }
2152
2163
  }
2164
+ this.silentDetections.add(uuid);
2153
2165
  if (trustSessionProtocol) {
2154
2166
  this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
2155
2167
  }
@@ -2161,6 +2173,38 @@ class ReactNativeBleTransport {
2161
2173
  throw this.createProtocolDetectionError();
2162
2174
  });
2163
2175
  }
2176
+ wakeSilentProtocolV1Device(uuid, probeOrder) {
2177
+ return __awaiter(this, void 0, void 0, function* () {
2178
+ if (!this._messages)
2179
+ return;
2180
+ if (!probeOrder.includes('V1'))
2181
+ return;
2182
+ if (!this.silentDetections.has(uuid))
2183
+ return;
2184
+ if (this.protocolWakeAttempts.has(uuid))
2185
+ return;
2186
+ if (!transportCache[uuid])
2187
+ return;
2188
+ this.protocolWakeAttempts.add(uuid);
2189
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
2190
+ connectIdSuffix: uuid.slice(-8),
2191
+ });
2192
+ try {
2193
+ this.probingProtocols.set(uuid, 'V1');
2194
+ yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2195
+ }
2196
+ catch (error) {
2197
+ if (shouldRethrowProtocolProbeError(error)) {
2198
+ this.clearProbeProtocol(uuid, 'V1');
2199
+ throw error;
2200
+ }
2201
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
2202
+ }
2203
+ finally {
2204
+ this.clearProbeProtocol(uuid, 'V1');
2205
+ }
2206
+ });
2207
+ }
2164
2208
  resetProbeStateAfterProtocolProbe(uuid, protocol) {
2165
2209
  var _a, _b, _c;
2166
2210
  return __awaiter(this, void 0, void 0, function* () {
@@ -2478,11 +2522,11 @@ class ReactNativeBleTransport {
2478
2522
  const transport = this.getCachedTransport(uuid);
2479
2523
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize))
2480
2524
  return;
2481
- const { device: refreshedDevice } = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
2525
+ const { device: refreshedDevice, abandoned } = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
2482
2526
  transport.device = refreshedDevice;
2483
2527
  transport.mtuSize =
2484
2528
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
2485
- if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2529
+ if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2486
2530
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`);
2487
2531
  }
2488
2532
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.2-alpha.114",
3
+ "version": "1.2.2-alpha.115",
4
4
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,11 +20,11 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-core": "1.2.2-alpha.114",
24
- "@onekeyfe/hd-shared": "1.2.2-alpha.114",
25
- "@onekeyfe/hd-transport": "1.2.2-alpha.114",
23
+ "@onekeyfe/hd-core": "1.2.2-alpha.115",
24
+ "@onekeyfe/hd-shared": "1.2.2-alpha.115",
25
+ "@onekeyfe/hd-transport": "1.2.2-alpha.115",
26
26
  "@onekeyfe/react-native-ble-utils": "0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "6490382eeeb6bda264b71c7e9bf96143840ee1cb"
29
+ "gitHead": "a115ca6998586c05c22996f278091778fd5471f1"
30
30
  }
@@ -169,13 +169,11 @@ const createHarness = ({
169
169
  serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
170
170
  isConnected: jest.fn(() => Promise.resolve(true)),
171
171
  cancelConnection: jest.fn(() => Promise.resolve()),
172
- connect: jest.fn(),
173
172
  onDisconnected: jest.fn(callback => {
174
173
  disconnectCallback = callback;
175
174
  return { remove: jest.fn() };
176
175
  }),
177
176
  } as any;
178
- device.connect.mockResolvedValue(device);
179
177
  device.requestMTU = jest.fn(() => Promise.resolve(device));
180
178
  device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
181
179
  const bleManager = {
@@ -1128,53 +1126,86 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1128
1126
  await transport.release(uuid, true);
1129
1127
  });
1130
1128
 
1131
- test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
1129
+ test('drops the link when a stalled MTU negotiation is abandoned', async () => {
1132
1130
  const { transport, uuid, device, bleManager } = createHarness();
1133
1131
  device.mtu = 23;
1134
1132
  device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
1133
+ const { resolveCharacteristics } = transport as any;
1135
1134
 
1136
- await expect(transport.acquire({ uuid })).resolves.toEqual({
1137
- uuid,
1138
- protocolType: 'V2',
1135
+ // cancelTransaction only disposes the JS subscription: RxAndroidBle releases the
1136
+ // connection's serial queue from onComplete/onError alone, so the abandoned exchange
1137
+ // keeps it until its own native timeout. Service discovery must not queue behind it.
1138
+ await expect(transport.acquire({ uuid })).rejects.toMatchObject({
1139
+ errorCode: HardwareErrorCode.BleConnectedError,
1139
1140
  });
1140
1141
  const transactionId = device.requestMTU.mock.calls[0]?.[1];
1141
1142
  expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
1142
1143
  expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
1143
- expect(device.cancelConnection).toHaveBeenCalled();
1144
- expect(device.connect).toHaveBeenCalledWith(
1145
- expect.objectContaining({ timeout: expect.any(Number) })
1146
- );
1147
- expect(device.connect.mock.calls.at(-1)?.[0]).not.toHaveProperty('requestMTU');
1148
- expect(device.requestMTU).toHaveBeenCalledTimes(1);
1149
- expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
1150
- await transport.release(uuid, true);
1144
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
1145
+ expect(resolveCharacteristics).not.toHaveBeenCalled();
1151
1146
  }, 10_000);
1152
1147
 
1153
- test('does not request MTU again after connect falls back without requestMTU', async () => {
1154
- const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
1148
+ test('skips the MTU refresh after an MTU-bearing connect was cancelled', async () => {
1155
1149
  const { transport, uuid, device } = createHarness();
1156
1150
  device.mtu = 23;
1157
- device.isConnected.mockResolvedValueOnce(false).mockResolvedValue(true);
1158
- device.connect
1159
- .mockRejectedValueOnce(
1160
- Object.assign(new BleErrorMock('Operation was cancelled'), {
1161
- errorCode: BleErrorCode.OperationCancelled,
1162
- })
1163
- )
1164
- .mockResolvedValue(device);
1151
+ let connected = false;
1152
+ device.isConnected.mockImplementation(() => Promise.resolve(connected));
1153
+ const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
1154
+ device.connect = jest
1155
+ .fn()
1156
+ .mockRejectedValueOnce(cancelled)
1157
+ .mockImplementation(() => {
1158
+ connected = true;
1159
+ return Promise.resolve(device);
1160
+ });
1165
1161
 
1166
1162
  await expect(transport.acquire({ uuid })).resolves.toEqual({
1167
1163
  uuid,
1168
1164
  protocolType: 'V2',
1169
1165
  });
1166
+
1167
+ // ble-plx applies the connect timeout to establishConnection -> refreshGatt ->
1168
+ // requestMtu as one chain, so the fallback means this peripheral did not finish the
1169
+ // MTU exchange. Asking again would only park a second abandoned exchange.
1170
1170
  expect(device.connect).toHaveBeenCalledTimes(2);
1171
- expect(device.connect.mock.calls[0][0]).toEqual(expect.objectContaining({ requestMTU: 247 }));
1172
- expect(device.connect.mock.calls[1][0]).not.toHaveProperty('requestMTU');
1173
1171
  expect(device.requestMTU).not.toHaveBeenCalled();
1174
1172
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
1175
1173
  await transport.release(uuid, true);
1176
1174
  });
1177
1175
 
1176
+ test('cleans up a cancelled native GATT setup, not only a timed-out one', async () => {
1177
+ const { transport, uuid, bleManager } = createHarness();
1178
+ const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
1179
+ (transport as any).resolveCharacteristics = jest.fn(() => Promise.reject(cancelled));
1180
+
1181
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1182
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
1183
+ });
1184
+
1185
+ test('spends one Initialize wake on the detection after a fully silent one', async () => {
1186
+ const { transport, uuid } = createHarness();
1187
+ const probes = transport as any;
1188
+ jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
1189
+ jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
1190
+ const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
1191
+
1192
+ // Nothing has gone silent yet, so the device keeps whatever session it has.
1193
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1194
+ expect(callProtocolV1).not.toHaveBeenCalled();
1195
+
1196
+ // The previous detection answered on no protocol, which is what a sleeping Classic
1197
+ // looks like, so this one wakes on the fresh link before probing.
1198
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1199
+ expect(callProtocolV1).toHaveBeenCalledTimes(1);
1200
+ expect(callProtocolV1.mock.calls[0]?.[1]).toBe('Initialize');
1201
+
1202
+ // Still silent: the wake is not repeated, so a device that is simply away is not
1203
+ // pushed into a fresh wallet session on every poll.
1204
+ callProtocolV1.mockClear();
1205
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1206
+ expect(callProtocolV1).not.toHaveBeenCalled();
1207
+ });
1208
+
1178
1209
  test('accepts a stable low MTU without the delayed refresh loop', async () => {
1179
1210
  const { transport, uuid, device } = createHarness();
1180
1211
  device.mtu = 185;
package/src/index.ts CHANGED
@@ -300,16 +300,22 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
300
300
  isConnectTimeoutError(error) || isWedgedBleSetupError(error);
301
301
  const isNativeOperationTimeoutError = (error: unknown): boolean =>
302
302
  (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
303
- const isMtuOrCancelledConnectError = (error: unknown): boolean => {
303
+ /**
304
+ * ble-plx reports its own expired budget as OperationCancelled, not OperationTimedOut:
305
+ * safeConnectToDevice disposes the operation chain and its doFinally reports a
306
+ * cancellation. Either code means the native layer abandoned work that still owned
307
+ * connection/GATT state, so both have to go through the stalled-connection cleanup.
308
+ * Only used where nothing else recovers the link — the connect path does its own
309
+ * ordered teardown before retrying, and routing it here too would both duplicate that
310
+ * and spend the manager-reset budget on a connect that is about to be retried.
311
+ */
312
+ const isAbandonedNativeOperationError = (error: unknown): boolean => {
304
313
  const errorCode = (error as { errorCode?: unknown })?.errorCode;
305
314
  return (
306
- errorCode === BleErrorCode.DeviceMTUChangeFailed ||
307
- errorCode === BleErrorCode.OperationCancelled
315
+ errorCode === BleErrorCode.OperationTimedOut || errorCode === BleErrorCode.OperationCancelled
308
316
  );
309
317
  };
310
318
 
311
- type NegotiatedMtuResult = { device: Device; timedOut: boolean };
312
-
313
319
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
314
320
 
315
321
  const tryToGetConfiguration = (device: Device) => {
@@ -321,13 +327,26 @@ const tryToGetConfiguration = (device: Device) => {
321
327
  return infos;
322
328
  };
323
329
 
330
+ type NegotiatedMtuResult = {
331
+ device: Device;
332
+ /**
333
+ * True when the bounded wait gave up while the native MTU exchange was still
334
+ * outstanding. `cancelTransaction` only disposes the JS-side subscription:
335
+ * RxAndroidBle releases a connection's serial operation queue from onComplete /
336
+ * onError alone, so an abandoned exchange keeps that queue until its own native
337
+ * timeout. Any GATT work issued before then — service discovery above all —
338
+ * queues behind it and cannot settle inside its own budget.
339
+ */
340
+ abandoned: boolean;
341
+ };
342
+
324
343
  const requestNegotiatedMtu = async (
325
344
  device: Device,
326
345
  stage: 'connected' | 'highThroughput',
327
346
  attempt: number,
328
347
  cancelTransaction?: (transactionId: string) => Promise<void> | void
329
348
  ): Promise<NegotiatedMtuResult> => {
330
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, timedOut: false };
349
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, abandoned: false };
331
350
 
332
351
  const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
333
352
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
@@ -350,7 +369,7 @@ const requestNegotiatedMtu = async (
350
369
  }, BLE_MTU_REQUEST_TIMEOUT_MS);
351
370
  }),
352
371
  ]);
353
- return { device: mtuDevice, timedOut: false };
372
+ return { device: mtuDevice, abandoned: false };
354
373
  } catch (error) {
355
374
  if (timedOut && cancelTransaction) {
356
375
  try {
@@ -376,10 +395,9 @@ const requestNegotiatedMtu = async (
376
395
  stage,
377
396
  attempt,
378
397
  actual: device.mtu,
379
- timedOut,
380
398
  error: error instanceof Error ? error.message : String(error),
381
399
  });
382
- return { device, timedOut };
400
+ return { device, abandoned: timedOut };
383
401
  } finally {
384
402
  if (timeoutId) clearTimeout(timeoutId);
385
403
  }
@@ -391,7 +409,7 @@ const resolveNegotiatedMtu = (
391
409
  ): Promise<NegotiatedMtuResult> =>
392
410
  shouldRefreshNegotiatedMtu(device.mtu)
393
411
  ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
394
- : Promise.resolve({ device, timedOut: false });
412
+ : Promise.resolve({ device, abandoned: false });
395
413
 
396
414
  type IOBleErrorRemap = Error | BleError | null | undefined;
397
415
 
@@ -475,6 +493,17 @@ export default class ReactNativeBleTransport {
475
493
  /** Consecutive detections that failed while trusting sessionProtocols. */
476
494
  private protocolReprobeFailures: Map<string, number> = new Map();
477
495
 
496
+ /** Endpoints whose last detection got no answer on any protocol. */
497
+ private silentDetections = new Set<string>();
498
+
499
+ /**
500
+ * Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
501
+ * on the device, and a failed detection tears the link down and reconnects on every
502
+ * poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
503
+ * detection succeeds, which re-arms it for the next time the device sleeps.
504
+ */
505
+ private protocolWakeAttempts = new Set<string>();
506
+
478
507
  /**
479
508
  * Native encryption/pairing failures seen before Protocol V2 probe starts.
480
509
  * Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
@@ -1072,6 +1101,14 @@ export default class ReactNativeBleTransport {
1072
1101
  }
1073
1102
 
1074
1103
  let device: Device | null = null;
1104
+ /**
1105
+ * Set when an MTU-bearing connect had to fall back to `fallbackConnectOptions`.
1106
+ * ble-plx applies `connectOptions.timeout` to the whole establishConnection ->
1107
+ * refreshGatt -> requestMtu chain, so that fallback means the peripheral did not
1108
+ * finish the MTU exchange on this link. Asking again below would only park a
1109
+ * second abandoned exchange on the connection's serial queue.
1110
+ */
1111
+ let mtuHandshakeRefused = false;
1075
1112
 
1076
1113
  if (forceCleanRunPromise && this.runPromise) {
1077
1114
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
@@ -1082,7 +1119,6 @@ export default class ReactNativeBleTransport {
1082
1119
  }
1083
1120
 
1084
1121
  const blePlxManager = await this.getPlxManager();
1085
- let skipPostConnectMtu = false;
1086
1122
  try {
1087
1123
  await subscribeBleOn(blePlxManager);
1088
1124
  } catch (error) {
@@ -1134,9 +1170,18 @@ export default class ReactNativeBleTransport {
1134
1170
  if (shouldRethrowBleSetupError(e)) {
1135
1171
  throw e;
1136
1172
  }
1137
- if (isMtuOrCancelledConnectError(e)) {
1138
- skipPostConnectMtu = true;
1173
+ if (
1174
+ e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
1175
+ e.errorCode === BleErrorCode.OperationCancelled
1176
+ ) {
1139
1177
  Log?.debug('first try to reconnect without params');
1178
+ mtuHandshakeRefused = true;
1179
+ // The disposed chain can still own native connection state, and the retry
1180
+ // would otherwise run on top of a half-open GATT client.
1181
+ await this.runBestEffortNativeOperation(
1182
+ 'connect cancelled: cancel manager connection',
1183
+ () => blePlxManager.cancelDeviceConnection(uuid)
1184
+ );
1140
1185
  device = await this.connectWithTimeout(uuid, () =>
1141
1186
  blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
1142
1187
  );
@@ -1166,9 +1211,18 @@ export default class ReactNativeBleTransport {
1166
1211
  if (shouldRethrowBleSetupError(e)) {
1167
1212
  throw e;
1168
1213
  }
1169
- if (isMtuOrCancelledConnectError(e)) {
1170
- skipPostConnectMtu = true;
1214
+ if (
1215
+ e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
1216
+ e.errorCode === BleErrorCode.OperationCancelled
1217
+ ) {
1171
1218
  Log?.debug('second try to reconnect without params');
1219
+ mtuHandshakeRefused = true;
1220
+ // The disposed chain can still own native connection state, and the retry
1221
+ // would otherwise run on top of a half-open GATT client.
1222
+ await this.runBestEffortNativeOperation(
1223
+ 'connect cancelled: cancel device connection',
1224
+ () => disconnectedDevice.cancelConnection()
1225
+ );
1172
1226
  try {
1173
1227
  device = await this.connectWithTimeout(uuid, () =>
1174
1228
  disconnectedDevice.connect(fallbackConnectOptions)
@@ -1209,26 +1263,27 @@ export default class ReactNativeBleTransport {
1209
1263
  throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
1210
1264
  }
1211
1265
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1212
- // Match 1.1.31: MTU is a connect() best-effort. If connect already fell back
1213
- // without requestMTU, do not put another requestMTU on the native serial
1214
- // queue — that is what wedges GATT after Account#2 reconnect.
1215
- if (!skipPostConnectMtu) {
1216
- const mtuResult = await resolveNegotiatedMtu(device, transactionId =>
1266
+ if (mtuHandshakeRefused) {
1267
+ Log?.debug('[ReactNativeBleTransport] skipping MTU refresh after cancelled MTU connect', {
1268
+ connectIdSuffix: uuid.slice(-8),
1269
+ actual: device.mtu,
1270
+ });
1271
+ } else {
1272
+ const negotiatedMtu = await resolveNegotiatedMtu(device, transactionId =>
1217
1273
  blePlxManager.cancelTransaction(transactionId)
1218
1274
  );
1219
- device = mtuResult.device;
1220
- if (mtuResult.timedOut) {
1221
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1222
- Log?.debug(
1223
- '[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU'
1224
- );
1225
- const timedOutDevice = device;
1226
- await this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () =>
1227
- timedOutDevice.cancelConnection()
1228
- );
1229
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1230
- device = await this.connectWithTimeout(uuid, () =>
1231
- timedOutDevice.connect(fallbackConnectOptions)
1275
+ device = negotiatedMtu.device;
1276
+ if (negotiatedMtu.abandoned) {
1277
+ // Service discovery would queue behind the abandoned exchange and miss its own
1278
+ // budget. Drop the link so the next attempt starts on a queue nothing holds.
1279
+ await this.runNativeTeardown(uuid, blePlxManager, async () => {
1280
+ await this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () =>
1281
+ blePlxManager.cancelDeviceConnection(uuid)
1282
+ );
1283
+ });
1284
+ throw ERRORS.TypedError(
1285
+ HardwareErrorCode.BleConnectedError,
1286
+ 'BLE MTU negotiation abandoned, reconnecting'
1232
1287
  );
1233
1288
  }
1234
1289
  }
@@ -2127,7 +2182,7 @@ export default class ReactNativeBleTransport {
2127
2182
  succeeded = true;
2128
2183
  return result;
2129
2184
  } catch (error) {
2130
- if (timedOut || isNativeOperationTimeoutError(error)) {
2185
+ if (timedOut || isAbandonedNativeOperationError(error)) {
2131
2186
  const resetManager = this.abandonStalledConnection(
2132
2187
  uuid,
2133
2188
  timedOut ? 'gatt-backstop' : 'gatt-native'
@@ -2331,6 +2386,8 @@ export default class ReactNativeBleTransport {
2331
2386
  // Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
2332
2387
  // firmware reconnect after the native BLE manager is recreated.
2333
2388
  this.protocolReprobeFailures.clear();
2389
+ this.silentDetections.clear();
2390
+ this.protocolWakeAttempts.clear();
2334
2391
  this.writeTimeoutCounts.clear();
2335
2392
  this.connectionSetupTimeoutCounts.clear();
2336
2393
  this.monitorTokens.clear();
@@ -2442,6 +2499,8 @@ export default class ReactNativeBleTransport {
2442
2499
  reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
2443
2500
  const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
2444
2501
 
2502
+ await this.wakeSilentProtocolV1Device(uuid, probeOrder);
2503
+
2445
2504
  for (let i = 0; i < probeOrder.length; i += 1) {
2446
2505
  const protocol = probeOrder[i];
2447
2506
  if (i > 0) {
@@ -2463,6 +2522,8 @@ export default class ReactNativeBleTransport {
2463
2522
  this.confirmedProtocolV2.add(uuid);
2464
2523
  }
2465
2524
  this.protocolReprobeFailures.delete(uuid);
2525
+ this.silentDetections.delete(uuid);
2526
+ this.protocolWakeAttempts.delete(uuid);
2466
2527
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
2467
2528
  deviceId: uuid,
2468
2529
  protocol,
@@ -2472,6 +2533,10 @@ export default class ReactNativeBleTransport {
2472
2533
  }
2473
2534
  }
2474
2535
 
2536
+ // Nothing answered on any probed protocol. Remember it so the next detection, which
2537
+ // the caller reaches on a freshly reconnected link, can spend a wake first.
2538
+ this.silentDetections.add(uuid);
2539
+
2475
2540
  if (trustSessionProtocol) {
2476
2541
  // Still silent on its own protocol: count it, and let the streak expire the
2477
2542
  // shortcut so a device that genuinely switched protocols is found again.
@@ -2485,6 +2550,47 @@ export default class ReactNativeBleTransport {
2485
2550
  throw this.createProtocolDetectionError();
2486
2551
  }
2487
2552
 
2553
+ /**
2554
+ * Sent once before probing when the previous detection on this endpoint got no answer
2555
+ * at all. On Classic-family firmware that silence is also what a sleeping device looks
2556
+ * like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
2557
+ * sits in the screensaver loop, whose host-message filter accepts only Initialize and
2558
+ * the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
2559
+ * amount of probing can bring the device back.
2560
+ *
2561
+ * Initialize is the one message that breaks that loop, but it also starts a fresh
2562
+ * wallet session, which is why it is not the probe itself — a bare Initialize on every
2563
+ * acquire would drop a hidden-wallet session before Core can restore it. Requiring a
2564
+ * fully silent previous detection keeps it to devices with no session left to protect.
2565
+ *
2566
+ * The firmware consumes the wake to leave its loop and does not reliably answer it, so
2567
+ * the result is ignored: the probes that follow are what decide the protocol.
2568
+ */
2569
+ private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
2570
+ if (!this._messages) return;
2571
+ if (!probeOrder.includes('V1')) return;
2572
+ if (!this.silentDetections.has(uuid)) return;
2573
+ if (this.protocolWakeAttempts.has(uuid)) return;
2574
+ if (!transportCache[uuid]) return;
2575
+ this.protocolWakeAttempts.add(uuid);
2576
+
2577
+ Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
2578
+ connectIdSuffix: uuid.slice(-8),
2579
+ });
2580
+ try {
2581
+ this.probingProtocols.set(uuid, 'V1');
2582
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2583
+ } catch (error) {
2584
+ if (shouldRethrowProtocolProbeError(error)) {
2585
+ this.clearProbeProtocol(uuid, 'V1');
2586
+ throw error;
2587
+ }
2588
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
2589
+ } finally {
2590
+ this.clearProbeProtocol(uuid, 'V1');
2591
+ }
2592
+ }
2593
+
2488
2594
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
2489
2595
  const transport = transportCache[uuid];
2490
2596
  await this.protocolV2Links.invalidateLink(
@@ -2864,7 +2970,7 @@ export default class ReactNativeBleTransport {
2864
2970
  const transport = this.getCachedTransport(uuid);
2865
2971
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
2866
2972
 
2867
- const { device: refreshedDevice } = await requestNegotiatedMtu(
2973
+ const { device: refreshedDevice, abandoned } = await requestNegotiatedMtu(
2868
2974
  transport.device,
2869
2975
  'highThroughput',
2870
2976
  1,
@@ -2874,7 +2980,7 @@ export default class ReactNativeBleTransport {
2874
2980
  transport.mtuSize =
2875
2981
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
2876
2982
 
2877
- if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2983
+ if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2878
2984
  throw ERRORS.TypedError(
2879
2985
  HardwareErrorCode.BleConnectedError,
2880
2986
  `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`