@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE.txt +21 -0
- package/lib/BleTransport.d.ts +67 -16
- package/lib/BleTransport.d.ts.map +1 -1
- package/lib/BleTransport.js +187 -79
- package/lib/BleTransport.js.map +1 -1
- package/lib/BleTransport.test.js +103 -15
- package/lib/BleTransport.test.js.map +1 -1
- package/lib/remapErrors.d.ts +1 -2
- package/lib/remapErrors.d.ts.map +1 -1
- package/lib/remapErrors.js.map +1 -1
- package/lib-es/BleTransport.d.ts +67 -16
- package/lib-es/BleTransport.d.ts.map +1 -1
- package/lib-es/BleTransport.js +191 -83
- package/lib-es/BleTransport.js.map +1 -1
- package/lib-es/BleTransport.test.js +104 -16
- package/lib-es/BleTransport.test.js.map +1 -1
- package/lib-es/remapErrors.d.ts +1 -2
- package/lib-es/remapErrors.d.ts.map +1 -1
- package/lib-es/remapErrors.js.map +1 -1
- package/package.json +9 -7
- package/src/BleTransport.test.ts +116 -15
- package/src/BleTransport.ts +278 -100
- package/src/remapErrors.ts +2 -1
package/src/BleTransport.ts
CHANGED
|
@@ -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 {
|
|
37
|
-
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
265
|
-
let
|
|
266
|
-
|
|
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
|
-
|
|
311
|
+
writableWithResponseCharacteristic = c;
|
|
271
312
|
} else if (c.uuid === writeCmdUuid) {
|
|
272
|
-
|
|
313
|
+
writableWithoutResponseCharacteristic = c;
|
|
273
314
|
} else if (c.uuid === notifyUuid) {
|
|
274
|
-
|
|
315
|
+
notifiableCharacteristic = c;
|
|
275
316
|
}
|
|
276
317
|
}
|
|
277
318
|
|
|
278
|
-
if (!
|
|
319
|
+
if (!writableWithResponseCharacteristic) {
|
|
279
320
|
throw new TransportError("write characteristic not found", "BLECharacteristicNotFound");
|
|
280
321
|
}
|
|
281
322
|
|
|
282
|
-
if (!
|
|
323
|
+
if (!notifiableCharacteristic) {
|
|
283
324
|
throw new TransportError("notify characteristic not found", "BLECharacteristicNotFound");
|
|
284
325
|
}
|
|
285
326
|
|
|
286
|
-
if (!
|
|
327
|
+
if (!writableWithResponseCharacteristic.isWritableWithResponse) {
|
|
287
328
|
throw new TransportError(
|
|
288
|
-
"
|
|
329
|
+
"The writable-with-response characteristic is not writable with response",
|
|
289
330
|
"BLECharacteristicInvalid",
|
|
290
331
|
);
|
|
291
332
|
}
|
|
292
333
|
|
|
293
|
-
if (!
|
|
334
|
+
if (!notifiableCharacteristic.isNotifiable) {
|
|
294
335
|
throw new TransportError("notify characteristic not notifiable", "BLECharacteristicInvalid");
|
|
295
336
|
}
|
|
296
337
|
|
|
297
|
-
if (
|
|
298
|
-
if (!
|
|
338
|
+
if (writableWithoutResponseCharacteristic) {
|
|
339
|
+
if (!writableWithoutResponseCharacteristic.isWritableWithoutResponse) {
|
|
299
340
|
throw new TransportError(
|
|
300
|
-
"
|
|
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
|
-
|
|
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(
|
|
324
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
618
|
+
// Observable emitting data received from the device via BLE
|
|
619
|
+
notifyObservable: Observable<Buffer | Error>;
|
|
553
620
|
notYetDisconnected = true;
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
notifyObservable: Observable<
|
|
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.
|
|
569
|
-
this.
|
|
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
|
-
*
|
|
580
|
-
*
|
|
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
|
|
583
|
-
*
|
|
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 = (
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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
|
|
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
|
-
*
|
|
689
|
-
*
|
|
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
|
|
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.
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
874
|
+
tracer.withType("ble-frame").trace("=> " + buffer.toString("hex"));
|
|
699
875
|
} catch (error: unknown) {
|
|
700
|
-
|
|
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.
|
|
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.
|
|
905
|
+
BleTransport.disconnectDevice(this.id, tracer.getContext())
|
|
728
906
|
.catch(() => {})
|
|
729
907
|
.finally(resolve);
|
|
730
908
|
} else {
|
package/src/remapErrors.ts
CHANGED
|
@@ -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
|
|