@ledgerhq/react-native-hw-transport-ble 6.31.0 → 6.32.0-next.0

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.
@@ -1,3 +1,4 @@
1
+ import { v4 as uuid } from "uuid";
1
2
  import Transport from "@ledgerhq/hw-transport";
2
3
  import type {
3
4
  Subscription as TransportSubscription,
@@ -13,7 +14,6 @@ import type {
13
14
  // See: https://github.com/LedgerHQ/ledger-live/pull/879
14
15
  import { sendAPDU } from "@ledgerhq/devices/lib/ble/sendAPDU";
15
16
  import { receiveAPDU } from "@ledgerhq/devices/lib/ble/receiveAPDU";
16
-
17
17
  import {
18
18
  BleManager,
19
19
  ConnectionPriority,
@@ -33,8 +33,28 @@ import {
33
33
  } from "@ledgerhq/devices";
34
34
  import type { DeviceModel } from "@ledgerhq/devices";
35
35
  import { trace, LocalTracer, TraceContext } from "@ledgerhq/logs";
36
- import { Observable, defer, merge, from, of, throwError, Observer, firstValueFrom } from "rxjs";
37
- import { share, ignoreElements, first, map, tap, catchError } from "rxjs/operators";
36
+ import {
37
+ Observable,
38
+ defer,
39
+ merge,
40
+ from,
41
+ of,
42
+ throwError,
43
+ Observer,
44
+ firstValueFrom,
45
+ TimeoutError,
46
+ SchedulerLike,
47
+ } from "rxjs";
48
+ import {
49
+ share,
50
+ ignoreElements,
51
+ first,
52
+ map,
53
+ tap,
54
+ catchError,
55
+ timeout,
56
+ finalize,
57
+ } from "rxjs/operators";
38
58
  import {
39
59
  CantOpenDevice,
40
60
  TransportError,
@@ -42,10 +62,16 @@ import {
42
62
  PairingFailed,
43
63
  PeerRemovedPairing,
44
64
  HwTransportError,
65
+ TransportExchangeTimeoutError,
45
66
  } from "@ledgerhq/errors";
46
67
  import { monitorCharacteristic } from "./monitorCharacteristic";
47
68
  import { awaitsBleOn } from "./awaitsBleOn";
48
- import { decoratePromiseErrors, remapError, mapBleErrorToHwTransportError } from "./remapErrors";
69
+ import {
70
+ decoratePromiseErrors,
71
+ remapError,
72
+ mapBleErrorToHwTransportError,
73
+ IOBleErrorRemap,
74
+ } from "./remapErrors";
49
75
  import { ReconnectionConfig } from "./types";
50
76
 
51
77
  const LOG_TYPE = "ble-verbose";
@@ -123,8 +149,10 @@ const clearDisconnectTimeout = (deviceId: string, context?: TraceContext): void
123
149
  *
124
150
  * @param deviceOrId
125
151
  * @param needsReconnect
126
- * @param timeoutMs TODO: to keep, used in a separate PR
152
+ * @param timeoutMs Optional Timeout (in ms) applied during the connection with the device
127
153
  * @param context Optional tracing/log context
154
+ * @param injectedDependencies Contains optional injected dependencies used by the transport implementation
155
+ * - rxjsScheduler: dependency injected RxJS scheduler to control time. Default AsyncScheduler.
128
156
  * @returns A BleTransport instance
129
157
  */
130
158
  async function open(
@@ -132,6 +160,7 @@ async function open(
132
160
  needsReconnect: boolean,
133
161
  timeoutMs?: number,
134
162
  context?: TraceContext,
163
+ { rxjsScheduler }: { rxjsScheduler?: SchedulerLike } = {},
135
164
  ) {
136
165
  const tracer = new LocalTracer(LOG_TYPE, context);
137
166
  let device: Device;
@@ -179,13 +208,16 @@ async function open(
179
208
 
180
209
  // Nb ConnectionOptions dropped since it's not used internally by ble-plx.
181
210
  try {
182
- device = await bleManagerInstance().connectToDevice(deviceOrId, connectOptions);
211
+ device = await bleManagerInstance().connectToDevice(deviceOrId, {
212
+ ...connectOptions,
213
+ timeout: timeoutMs,
214
+ });
183
215
  } catch (e: any) {
184
216
  tracer.trace(`Error code: ${e.errorCode}`);
185
217
  if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
186
218
  // If the MTU update did not work, we try to connect without requesting for a specific MTU
187
219
  connectOptions = {};
188
- device = await bleManagerInstance().connectToDevice(deviceOrId);
220
+ device = await bleManagerInstance().connectToDevice(deviceOrId, { timeout: timeoutMs });
189
221
  } else {
190
222
  throw e;
191
223
  }
@@ -203,7 +235,7 @@ async function open(
203
235
  if (!(await device.isConnected())) {
204
236
  tracer.trace(`Device found but not connected. connecting...`, { timeoutMs, connectOptions });
205
237
  try {
206
- await device.connect({ ...connectOptions });
238
+ await device.connect({ ...connectOptions, timeout: timeoutMs });
207
239
  } catch (error: any) {
208
240
  tracer.trace(`Connect error`, { error });
209
241
  if (error.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
@@ -221,6 +253,14 @@ async function open(
221
253
  deviceName: device.name,
222
254
  productName,
223
255
  });
256
+ }
257
+ // This case should not happen, but recording logs to be warned if we see it in production
258
+ else if (error.errorCode === BleErrorCode.DeviceAlreadyConnected) {
259
+ tracer.trace(`Device already connected, while it was not supposed to`, {
260
+ error,
261
+ });
262
+
263
+ throw remapError(error);
224
264
  } else {
225
265
  throw remapError(error);
226
266
  }
@@ -261,52 +301,56 @@ async function open(
261
301
  throw new TransportError("service not found", "BLEServiceNotFound");
262
302
  }
263
303
 
264
- let writeC: Characteristic | null | undefined;
265
- let writeCmdC: Characteristic | undefined;
266
- let notifyC: Characteristic | null | undefined;
304
+ let writableWithResponseCharacteristic: Characteristic | null | undefined;
305
+ let writableWithoutResponseCharacteristic: Characteristic | undefined;
306
+ // A characteristic that can monitor value changes
307
+ let notifiableCharacteristic: Characteristic | null | undefined;
267
308
 
268
309
  for (const c of characteristics) {
269
310
  if (c.uuid === writeUuid) {
270
- writeC = c;
311
+ writableWithResponseCharacteristic = c;
271
312
  } else if (c.uuid === writeCmdUuid) {
272
- writeCmdC = c;
313
+ writableWithoutResponseCharacteristic = c;
273
314
  } else if (c.uuid === notifyUuid) {
274
- notifyC = c;
315
+ notifiableCharacteristic = c;
275
316
  }
276
317
  }
277
318
 
278
- if (!writeC) {
319
+ if (!writableWithResponseCharacteristic) {
279
320
  throw new TransportError("write characteristic not found", "BLECharacteristicNotFound");
280
321
  }
281
322
 
282
- if (!notifyC) {
323
+ if (!notifiableCharacteristic) {
283
324
  throw new TransportError("notify characteristic not found", "BLECharacteristicNotFound");
284
325
  }
285
326
 
286
- if (!writeC.isWritableWithResponse) {
327
+ if (!writableWithResponseCharacteristic.isWritableWithResponse) {
287
328
  throw new TransportError(
288
- "write characteristic not writableWithResponse",
329
+ "The writable-with-response characteristic is not writable with response",
289
330
  "BLECharacteristicInvalid",
290
331
  );
291
332
  }
292
333
 
293
- if (!notifyC.isNotifiable) {
334
+ if (!notifiableCharacteristic.isNotifiable) {
294
335
  throw new TransportError("notify characteristic not notifiable", "BLECharacteristicInvalid");
295
336
  }
296
337
 
297
- if (writeCmdC) {
298
- if (!writeCmdC.isWritableWithoutResponse) {
338
+ if (writableWithoutResponseCharacteristic) {
339
+ if (!writableWithoutResponseCharacteristic.isWritableWithoutResponse) {
299
340
  throw new TransportError(
300
- "write cmd characteristic not writableWithoutResponse",
341
+ "The writable-without-response characteristic is not writable without response",
301
342
  "BLECharacteristicInvalid",
302
343
  );
303
344
  }
304
345
  }
305
346
 
306
347
  tracer.trace(`device.mtu=${device.mtu}`);
307
- const notifyObservable = monitorCharacteristic(notifyC, context).pipe(
348
+
349
+ // Inits the observable that will emit received data from the device via BLE
350
+ const notifyObservable = monitorCharacteristic(notifiableCharacteristic, context).pipe(
308
351
  catchError(e => {
309
352
  // LL-9033 fw 2.0.2 introduced this case, we silence the inner unhandled error.
353
+ // It will be handled when negotiating the MTU in `inferMTU` but will be ignored in other cases.
310
354
  const msg = String(e);
311
355
  return msg.includes("notify change failed")
312
356
  ? of(new PairingFailed(msg))
@@ -316,19 +360,34 @@ async function open(
316
360
  if (value instanceof PairingFailed) return;
317
361
  trace({ type: "ble-frame", message: `<= ${value.toString("hex")}`, context });
318
362
  }),
363
+ // Returns a new Observable that multicasts (shares) the original Observable.
364
+ // As long as there is at least one Subscriber this Observable will be subscribed and emitting data.
319
365
  share(),
320
366
  );
367
+
368
+ // Keeps the input from the device observable alive (multicast observable)
321
369
  const notif = notifyObservable.subscribe();
322
370
 
323
- const transport = new BleTransport(device, writeC, writeCmdC, notifyObservable, deviceModel, {
324
- context,
325
- });
371
+ const transport = new BleTransport(
372
+ device,
373
+ writableWithResponseCharacteristic,
374
+ writableWithoutResponseCharacteristic,
375
+ notifyObservable,
376
+ deviceModel,
377
+ {
378
+ context,
379
+ rxjsScheduler,
380
+ },
381
+ );
326
382
  tracer.trace(`New BleTransport created`);
327
383
 
328
384
  // Keeping it as a comment for now but if no new bluetooth issues occur, we will be able to remove it
329
385
  // await transport.requestConnectionPriority("High");
386
+
330
387
  // eslint-disable-next-line prefer-const
331
388
  let disconnectedSub: Subscription;
389
+
390
+ // Callbacks on `react-native-ble-plx` notifying the device has been disconnected
332
391
  const onDisconnect = (error: BleError | null) => {
333
392
  transport.isConnected = false;
334
393
  transport.notYetDisconnected = false;
@@ -338,7 +397,7 @@ async function open(
338
397
  clearDisconnectTimeout(transport.id);
339
398
  delete transportsCache[transport.id];
340
399
  tracer.trace(
341
- `On device disconnected callback: cleared cached transport for ${transport.id}, emitting Transport event "disconnect"`,
400
+ `On device disconnected callback: cleared cached transport for ${transport.id}, emitting Transport event "disconnect. Error: ${error}"`,
342
401
  { reason: error },
343
402
  );
344
403
  transport.emit("disconnect", error);
@@ -371,7 +430,8 @@ async function open(
371
430
  }
372
431
 
373
432
  if (needsReconnect) {
374
- await BleTransport.disconnect(transport.id).catch(() => {});
433
+ tracer.trace(`Device needs reconnection. Triggering a disconnect`);
434
+ await BleTransport.disconnectDevice(transport.id);
375
435
  await delay(reconnectionConfig.delayAfterFirstPairing);
376
436
  }
377
437
  } else {
@@ -467,7 +527,7 @@ export default class BleTransport extends Transport {
467
527
  if (devices.length) {
468
528
  tracer.trace("Disconnecting from all devices", { deviceCount: devices.length });
469
529
 
470
- await Promise.all(devices.map(d => BleTransport.disconnect(d.id).catch(() => {})));
530
+ await Promise.all(devices.map(d => BleTransport.disconnectDevice(d.id)));
471
531
  }
472
532
 
473
533
  if (unsubscribed) return;
@@ -513,34 +573,40 @@ export default class BleTransport extends Transport {
513
573
  * Opens a BLE transport
514
574
  *
515
575
  * @param {Device | string} deviceOrId
516
- * @param timeoutMs TODO: to keep, used in a separate PR
576
+ * @param timeoutMs Applied when trying to connect to a device
517
577
  * @param context An optional context object for log/tracing strategy
578
+ * @param injectedDependencies Contains optional injected dependencies used by the transport implementation
579
+ * - rxjsScheduler: dependency injected RxJS scheduler to control time. Default AsyncScheduler.
518
580
  */
519
581
  static async open(
520
582
  deviceOrId: Device | string,
521
583
  timeoutMs?: number,
522
584
  context?: TraceContext,
585
+ { rxjsScheduler }: { rxjsScheduler?: SchedulerLike } = {},
523
586
  ): Promise<BleTransport> {
524
- return open(deviceOrId, true, timeoutMs, context);
587
+ return open(deviceOrId, true, timeoutMs, context, { rxjsScheduler });
525
588
  }
526
589
 
527
590
  /**
528
591
  * Exposes method from the ble-plx library to disconnect a device
529
592
  *
530
593
  * Disconnects from {@link Device} if it's connected or cancels pending connection.
594
+ * A "disconnect" event will normally be emitted by the ble-plx lib once the device is disconnected.
595
+ * Errors are logged but silenced.
531
596
  */
532
- static disconnect = async (id: DeviceId, context?: TraceContext): Promise<void> => {
533
- trace({
534
- type: LOG_TYPE,
535
- message: `Trying to disconnect device ${id})`,
536
- context,
537
- });
538
- await bleManagerInstance().cancelDeviceConnection(id);
539
- trace({
540
- type: LOG_TYPE,
541
- message: `Device ${id} disconnected`,
542
- context,
543
- });
597
+ static disconnectDevice = async (id: DeviceId, context?: TraceContext): Promise<void> => {
598
+ const tracer = new LocalTracer(LOG_TYPE, context);
599
+ tracer.trace(`Trying to disconnect device ${id}`);
600
+
601
+ await bleManagerInstance()
602
+ .cancelDeviceConnection(id)
603
+ .catch(error => {
604
+ // Only log, ignore if disconnect did not work
605
+ tracer
606
+ .withType("ble-error")
607
+ .trace(`Error while trying to cancel device connection`, { error });
608
+ });
609
+ tracer.trace(`Device ${id} disconnected`);
544
610
  };
545
611
 
546
612
  device: Device;
@@ -549,26 +615,47 @@ export default class BleTransport extends Transport {
549
615
  id: string;
550
616
  isConnected = true;
551
617
  mtuSize = 20;
552
- notifyObservable: Observable<any>;
618
+ // Observable emitting data received from the device via BLE
619
+ notifyObservable: Observable<Buffer | Error>;
553
620
  notYetDisconnected = true;
554
- writeCharacteristic: Characteristic;
555
- writeCmdCharacteristic: Characteristic | undefined;
621
+ writableWithResponseCharacteristic: Characteristic;
622
+ writableWithoutResponseCharacteristic: Characteristic | undefined;
623
+ rxjsScheduler?: SchedulerLike;
624
+ // Transaction ids of communication operations that are currently pending
625
+ currentTransactionIds: Array<string>;
556
626
 
627
+ /**
628
+ * The static `open` function is used to handle BleTransport instantiation
629
+ *
630
+ * @param device
631
+ * @param writableWithResponseCharacteristic A BLE characteristic that we can write on,
632
+ * and that will be acknowledged in response from the device when it receives the written value.
633
+ * @param writableWithoutResponseCharacteristic A BLE characteristic that we can write on,
634
+ * and that will not be acknowledged in response from the device
635
+ * @param notifyObservable A multicast observable that emits messages received from the device
636
+ * @param deviceModel
637
+ * @param params Contains optional options and injected dependencies used by the transport implementation
638
+ * - abortTimeoutMs: stop the exchange after a given timeout. Another timeout exists
639
+ * to detect unresponsive device (see `unresponsiveTimeout`). This timeout aborts the exchange.
640
+ * - rxjsScheduler: dependency injected RxJS scheduler to control time. Default: AsyncScheduler.
641
+ */
557
642
  constructor(
558
643
  device: Device,
559
- writeCharacteristic: Characteristic,
560
- writeCmdCharacteristic: Characteristic | undefined,
561
- notifyObservable: Observable<any>,
644
+ writableWithResponseCharacteristic: Characteristic,
645
+ writableWithoutResponseCharacteristic: Characteristic | undefined,
646
+ notifyObservable: Observable<Buffer | Error>,
562
647
  deviceModel: DeviceModel,
563
- { context }: { context?: TraceContext } = {},
648
+ { context, rxjsScheduler }: { context?: TraceContext; rxjsScheduler?: SchedulerLike } = {},
564
649
  ) {
565
650
  super({ context, logType: LOG_TYPE });
566
651
  this.id = device.id;
567
652
  this.device = device;
568
- this.writeCharacteristic = writeCharacteristic;
569
- this.writeCmdCharacteristic = writeCmdCharacteristic;
653
+ this.writableWithResponseCharacteristic = writableWithResponseCharacteristic;
654
+ this.writableWithoutResponseCharacteristic = writableWithoutResponseCharacteristic;
570
655
  this.notifyObservable = notifyObservable;
571
656
  this.deviceModel = deviceModel;
657
+ this.rxjsScheduler = rxjsScheduler;
658
+ this.currentTransactionIds = [];
572
659
 
573
660
  clearDisconnectTimeout(this.id);
574
661
 
@@ -576,50 +663,111 @@ export default class BleTransport extends Transport {
576
663
  }
577
664
 
578
665
  /**
579
- * Send data to the device using a low level API.
580
- * It's recommended to use the "send" method for a higher level API.
666
+ * A message exchange (APDU request <-> response) with the device that can be aborted
667
+ *
668
+ * The message will be BLE-encoded/framed before being sent, and the response will be BLE-decoded.
581
669
  *
582
- * @param {Buffer} apdu - The data to send.
583
- * @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
670
+ * @param message A buffer (u8 array) of a none BLE-encoded message (an APDU for ex) to be sent to the device
671
+ * as a request
672
+ * @param options Contains optional options for the exchange function
673
+ * - abortTimeoutMs: stop the exchange after a given timeout. Another timeout exists
674
+ * to detect unresponsive device (see `unresponsiveTimeout`). This timeout aborts the exchange.
675
+ * @returns A promise that resolves with the response data from the device.
584
676
  */
585
- exchange = (apdu: Buffer): Promise<any> => {
677
+ exchange = (
678
+ message: Buffer,
679
+ { abortTimeoutMs }: { abortTimeoutMs?: number } = {},
680
+ ): Promise<Buffer> => {
586
681
  const tracer = this.tracer.withUpdatedContext({
587
682
  function: "exchange",
588
683
  });
589
- tracer.trace("Exchanging APDU ...");
590
-
591
- return this.exchangeAtomicImpl(async () => {
592
- try {
593
- const msgIn = apdu.toString("hex");
594
- tracer.withType("apdu").trace(`=> ${msgIn}`);
595
-
596
- const data = await firstValueFrom(
597
- merge(this.notifyObservable.pipe(receiveAPDU), sendAPDU(this.write, apdu, this.mtuSize)),
598
- );
599
-
600
- const msgOut = data.toString("hex");
601
- tracer.withType("apdu").trace(`<= ${msgOut}`);
684
+ tracer.trace("Exchanging APDU ...", { abortTimeoutMs });
685
+ tracer.withType("apdu").trace(`=> ${message.toString("hex")}`);
686
+
687
+ return this.exchangeAtomicImpl(() => {
688
+ return firstValueFrom(
689
+ // `sendApdu` will only emit if an error occurred, otherwise it will complete,
690
+ // while `receiveAPDU` will emit the full response.
691
+ // Consequently it monitors the response while being able to reject on an error from the send.
692
+ merge(
693
+ this.notifyObservable.pipe(data => receiveAPDU(data, { context: tracer.getContext() })),
694
+ sendAPDU(this.write, message, this.mtuSize, {
695
+ context: tracer.getContext(),
696
+ }),
697
+ ).pipe(
698
+ abortTimeoutMs ? timeout(abortTimeoutMs, this.rxjsScheduler) : tap(),
699
+ tap(data => {
700
+ tracer.withType("apdu").trace(`<= ${data.toString("hex")}`);
701
+ }),
702
+ catchError(async error => {
703
+ // Currently only 1 reason the exchange has been explicitly aborted (other than job and transport errors): a timeout
704
+ if (error instanceof TimeoutError) {
705
+ tracer.trace(
706
+ "Aborting due to timeout and trying to cancel all communication write of the current exchange",
707
+ {
708
+ abortTimeoutMs,
709
+ transactionIds: this.currentTransactionIds,
710
+ },
711
+ );
712
+
713
+ // No concurrent exchange should happen at the same time, so all pending operations are part of the same exchange
714
+ this.cancelPendingOperations();
715
+
716
+ throw new TransportExchangeTimeoutError("Exchange aborted due to timeout");
717
+ }
602
718
 
603
- return data;
604
- } catch (error: any) {
605
- tracer.withType("ble-error").trace(`Error while exchanging APDU`, { error });
719
+ tracer.withType("ble-error").trace(`Error while exchanging APDU`, { error });
606
720
 
607
- if (this.notYetDisconnected) {
608
- // in such case we will always disconnect because something is bad.
609
- await bleManagerInstance()
610
- .cancelDeviceConnection(this.id)
611
- .catch(() => {}); // but we ignore if disconnect worked.
612
- }
721
+ if (this.notYetDisconnected) {
722
+ // In such case we will always disconnect because something is bad.
723
+ // This sends a "disconnect" event
724
+ await BleTransport.disconnectDevice(this.id);
725
+ }
613
726
 
614
- const mappedError = remapError(error);
615
- tracer.trace("Error while exchanging APDU, mapped and throws following error", {
616
- mappedError,
617
- });
618
- throw mappedError;
619
- }
727
+ const mappedError = remapError(error as IOBleErrorRemap);
728
+ tracer.trace("Error while exchanging APDU, mapped and throws following error", {
729
+ mappedError,
730
+ });
731
+ throw mappedError;
732
+ }),
733
+ finalize(() => {
734
+ tracer.trace("Clearing current transaction ids", {
735
+ currentTransactionIds: this.currentTransactionIds,
736
+ });
737
+ this.clearCurrentTransactionIds();
738
+ }),
739
+ ),
740
+ );
620
741
  });
621
742
  };
622
743
 
744
+ /**
745
+ * Tries to cancel all operations that have a recorded transaction and are pending
746
+ *
747
+ * Cancelling transaction which doesn't exist is ignored.
748
+ *
749
+ * Note: cancelling `writableWithoutResponseCharacteristic.write...` will throw a `BleError` with code `OperationCancelled`
750
+ * but this error should be ignored. (In `exchange` our observable is unsubscribed before `cancelPendingOperations`
751
+ * is called so the error is ignored)
752
+ */
753
+ private cancelPendingOperations() {
754
+ for (const transactionId of this.currentTransactionIds) {
755
+ try {
756
+ this.tracer.trace("Cancelling operation", { transactionId });
757
+ bleManagerInstance().cancelTransaction(transactionId);
758
+ } catch (error) {
759
+ this.tracer.trace("Error while cancelling operation", { transactionId, error });
760
+ }
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Sets the collection of current transaction ids to an empty array
766
+ */
767
+ private clearCurrentTransactionIds() {
768
+ this.currentTransactionIds = [];
769
+ }
770
+
623
771
  /**
624
772
  * Negotiate with the device the maximum transfer unit for the ble frames
625
773
  * @returns Promise<number>
@@ -633,8 +781,13 @@ export default class BleTransport extends Transport {
633
781
  mtu = await firstValueFrom(
634
782
  merge(
635
783
  this.notifyObservable.pipe(
636
- tap(maybeError => {
637
- if (maybeError instanceof Error) throw maybeError;
784
+ map(maybeError => {
785
+ // Catches the PairingFailed Error that has only been emitted
786
+ if (maybeError instanceof Error) {
787
+ throw maybeError;
788
+ }
789
+
790
+ return maybeError;
638
791
  }),
639
792
  first(buffer => buffer.readUInt8(0) === 0x08),
640
793
  map(buffer => buffer.readUInt8(5)),
@@ -645,15 +798,16 @@ export default class BleTransport extends Transport {
645
798
  } catch (error: any) {
646
799
  this.tracer.withType("ble-error").trace("Error while inferring MTU", { mtu });
647
800
 
648
- await bleManagerInstance()
649
- .cancelDeviceConnection(this.id)
650
- .catch(() => {}); // but we ignore if disconnect worked.
801
+ await BleTransport.disconnectDevice(this.id);
651
802
 
652
803
  const mappedError = remapError(error);
653
804
  this.tracer.trace("Error while inferring APDU, mapped and throws following error", {
654
805
  mappedError,
655
806
  });
656
807
  throw mappedError;
808
+ } finally {
809
+ // When negotiating the MTU, a message is sent/written to the device, and a transaction id was associated to this write
810
+ this.clearCurrentTransactionIds();
657
811
  }
658
812
  });
659
813
 
@@ -685,19 +839,41 @@ export default class BleTransport extends Transport {
685
839
  /**
686
840
  * Do not call this directly unless you know what you're doing. Communication
687
841
  * with a Ledger device should be through the {@link exchange} method.
688
- * @param buffer
689
- * @param txid
842
+ *
843
+ * For each call a transaction id is added to the current stack of transaction ids.
844
+ * With this transaction id, a pending BLE communication operations can be cancelled.
845
+ * Note: each frame/packet of a longer BLE-encoded message to be sent should have their unique transaction id.
846
+ *
847
+ * @param buffer BLE-encoded packet to send to the device
848
+ * @param frameId Frame id to make `write` aware of a bigger message that this frame/packet is part of.
849
+ * Helps creating related a collection of transaction ids
690
850
  */
691
- write = async (buffer: Buffer, txid?: string | undefined): Promise<void> => {
851
+ write = async (buffer: Buffer): Promise<void> => {
852
+ const transactionId = uuid();
853
+ this.currentTransactionIds.push(transactionId);
854
+
855
+ const tracer = this.tracer.withUpdatedContext({ transactionId });
856
+ tracer.trace("Writing to device", {
857
+ willMessageBeAcked: !this.writableWithoutResponseCharacteristic,
858
+ });
859
+
692
860
  try {
693
- if (!this.writeCmdCharacteristic) {
694
- await this.writeCharacteristic.writeWithResponse(buffer.toString("base64"), txid);
861
+ if (!this.writableWithoutResponseCharacteristic) {
862
+ // The message will be acked in response by the device
863
+ await this.writableWithResponseCharacteristic.writeWithResponse(
864
+ buffer.toString("base64"),
865
+ transactionId,
866
+ );
695
867
  } else {
696
- await this.writeCmdCharacteristic.writeWithoutResponse(buffer.toString("base64"), txid);
868
+ // The message won't be acked in response by the device
869
+ await this.writableWithoutResponseCharacteristic.writeWithoutResponse(
870
+ buffer.toString("base64"),
871
+ transactionId,
872
+ );
697
873
  }
698
- this.tracer.withType("ble-frame").trace("=> " + buffer.toString("hex"));
874
+ tracer.withType("ble-frame").trace("=> " + buffer.toString("hex"));
699
875
  } catch (error: unknown) {
700
- this.tracer.trace("Error while writing APDU", { error });
876
+ tracer.trace("Error while writing APDU", { error });
701
877
  throw new DisconnectedDeviceDuringOperation(
702
878
  error instanceof Error ? error.message : `${error}`,
703
879
  );
@@ -713,7 +889,8 @@ export default class BleTransport extends Transport {
713
889
  * @returns {Promise<void>}
714
890
  */
715
891
  async close(): Promise<void> {
716
- this.tracer.trace("Closing, queuing a disconnect ...");
892
+ const tracer = this.tracer.withUpdatedContext({ function: "close" });
893
+ tracer.trace("Closing, queuing a disconnect with a timeout ...");
717
894
 
718
895
  let resolve: (value: void | PromiseLike<void>) => void;
719
896
  const disconnectPromise = new Promise<void>(innerResolve => {
@@ -723,8 +900,9 @@ export default class BleTransport extends Transport {
723
900
  clearDisconnectTimeout(this.id);
724
901
 
725
902
  this.disconnectTimeout = setTimeout(() => {
903
+ tracer.trace("Disconnect timeout has been reached ...");
726
904
  if (this.isConnected) {
727
- BleTransport.disconnect(this.id, this.tracer.getContext())
905
+ BleTransport.disconnectDevice(this.id, tracer.getContext())
728
906
  .catch(() => {})
729
907
  .finally(resolve);
730
908
  } else {
@@ -7,7 +7,8 @@ import {
7
7
  } from "@ledgerhq/errors";
8
8
  import { BleATTErrorCode, BleError, BleErrorCode } from "react-native-ble-plx";
9
9
 
10
- type IOBleErrorRemap = Error | BleError | null | undefined;
10
+ export type IOBleErrorRemap = Error | BleError | null | undefined;
11
+
11
12
  export const remapError = (error: IOBleErrorRemap): IOBleErrorRemap => {
12
13
  if (!error || !error.message) return error;
13
14