@ledgerhq/react-native-hw-transport-ble 6.28.3 → 6.28.4-nightly.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -0
- package/lib/BleTransport.d.ts +67 -37
- package/lib/BleTransport.d.ts.map +1 -1
- package/lib/BleTransport.js +257 -198
- package/lib/BleTransport.js.map +1 -1
- package/lib/BleTransport.test.d.ts +2 -0
- package/lib/BleTransport.test.d.ts.map +1 -0
- package/lib/BleTransport.test.js +370 -0
- package/lib/BleTransport.test.js.map +1 -0
- package/lib/remapErrors.d.ts +4 -0
- package/lib/remapErrors.d.ts.map +1 -1
- package/lib/remapErrors.js +20 -1
- package/lib/remapErrors.js.map +1 -1
- package/lib/types.d.ts +4 -0
- package/lib/types.d.ts.map +1 -1
- package/lib-es/BleTransport.d.ts +67 -37
- package/lib-es/BleTransport.d.ts.map +1 -1
- package/lib-es/BleTransport.js +257 -198
- package/lib-es/BleTransport.js.map +1 -1
- package/lib-es/BleTransport.test.d.ts +2 -0
- package/lib-es/BleTransport.test.d.ts.map +1 -0
- package/lib-es/BleTransport.test.js +365 -0
- package/lib-es/BleTransport.test.js.map +1 -0
- package/lib-es/remapErrors.d.ts +4 -0
- package/lib-es/remapErrors.d.ts.map +1 -1
- package/lib-es/remapErrors.js +19 -1
- package/lib-es/remapErrors.js.map +1 -1
- package/lib-es/types.d.ts +4 -0
- package/lib-es/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/BleTransport.test.ts +275 -0
- package/src/BleTransport.ts +250 -172
- package/src/remapErrors.ts +33 -2
- package/src/types.ts +5 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import BleTransport from "../src/BleTransport";
|
|
2
|
+
import { Subscription } from "rxjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* It is essential to mock the BLE component of a BLE transport to verify
|
|
6
|
+
* the reliability of the connect/reconnect/disconnect logic, which is decoupled
|
|
7
|
+
* from the managing logic of live-common or any other implementation.
|
|
8
|
+
* Although it may seem trivial, such an approach is necessary to ensure
|
|
9
|
+
* that the implementation is robust and dependable.
|
|
10
|
+
*
|
|
11
|
+
* To this end, a mocked react-native-ble-plx has been developed specifically
|
|
12
|
+
* to cover test cases. It should be noted that this mock is not comprehensive
|
|
13
|
+
* and may require further refinement to meet all requirements.
|
|
14
|
+
*/
|
|
15
|
+
jest.mock("react-native-ble-plx", () => {
|
|
16
|
+
// Set of callbacks that we can trigger from our tests.
|
|
17
|
+
const callbacks: { [key: string]: (...args: any[]) => void } = {};
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
BleErrorCode: {
|
|
21
|
+
ScanStartFailed: 0,
|
|
22
|
+
},
|
|
23
|
+
BleManager: function () {
|
|
24
|
+
const dynamicProps = {
|
|
25
|
+
isConnected: true,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
onStateChange: (callback) => {
|
|
30
|
+
setTimeout(() => callback("PoweredOn"), 500);
|
|
31
|
+
return new Subscription();
|
|
32
|
+
},
|
|
33
|
+
cancelDeviceConnection: async () => {
|
|
34
|
+
dynamicProps.isConnected = false;
|
|
35
|
+
callbacks?.onDisconnected(null);
|
|
36
|
+
},
|
|
37
|
+
devices: () => [],
|
|
38
|
+
connectedDevices: () => [],
|
|
39
|
+
connectToDevice: () => {
|
|
40
|
+
dynamicProps.isConnected = true;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isConnectable: null,
|
|
44
|
+
serviceData: null,
|
|
45
|
+
overflowServiceUUIDs: null,
|
|
46
|
+
txPowerLevel: null,
|
|
47
|
+
serviceUUIDs: null,
|
|
48
|
+
rssi: null,
|
|
49
|
+
mtu: 0,
|
|
50
|
+
name: "Ledger Stax 2783",
|
|
51
|
+
localName: null,
|
|
52
|
+
id: "20EDD96F-7430-6E33-AB22-DD8AAB857CD4",
|
|
53
|
+
manufacturerData: null,
|
|
54
|
+
solicitedServiceUUIDs: null,
|
|
55
|
+
|
|
56
|
+
isConnected: () => {
|
|
57
|
+
return dynamicProps.isConnected;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
onDisconnected: (callback) => {
|
|
61
|
+
callbacks["onDisconnected"] = () => callback(null); // Disconnect without an error
|
|
62
|
+
return new Subscription();
|
|
63
|
+
},
|
|
64
|
+
discoverAllServicesAndCharacteristics: () => {},
|
|
65
|
+
characteristicsForService: (uuid) => {
|
|
66
|
+
if (uuid === "13d63400-2c97-6004-0000-4c6564676572") {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
// Device responses
|
|
70
|
+
serviceUUID: "13d63400-2c97-6004-0000-4c6564676572",
|
|
71
|
+
isIndicatable: false,
|
|
72
|
+
isNotifiable: true,
|
|
73
|
+
isWritableWithoutResponse: false,
|
|
74
|
+
isWritableWithResponse: false,
|
|
75
|
+
serviceID: 105553179758272,
|
|
76
|
+
isReadable: false,
|
|
77
|
+
deviceID: "20EDD96F-7430-6E33-AB22-DD8AAB857CD4",
|
|
78
|
+
isNotifying: false,
|
|
79
|
+
value:
|
|
80
|
+
"BQAAACMzIAAECTEuMC4wLXJjOQTuAAALBDUuMTUEMC4zNQEAAQCQAA==",
|
|
81
|
+
id: 105553399124864,
|
|
82
|
+
uuid: "13d63400-2c97-6004-0001-4c6564676572",
|
|
83
|
+
monitor: (cb) => {
|
|
84
|
+
callbacks["onDeviceResponse"] = cb;
|
|
85
|
+
return new Subscription();
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
// Write
|
|
90
|
+
isNotifying: false,
|
|
91
|
+
value: null,
|
|
92
|
+
isIndicatable: false,
|
|
93
|
+
id: 105553399131872,
|
|
94
|
+
uuid: "13d63400-2c97-6004-0002-4c6564676572",
|
|
95
|
+
isReadable: false,
|
|
96
|
+
deviceID: "20EDD96F-7430-6E33-AB22-DD8AAB857CD4",
|
|
97
|
+
serviceID: 105553179758272,
|
|
98
|
+
serviceUUID: "13d63400-2c97-6004-0000-4c6564676572",
|
|
99
|
+
isWritableWithoutResponse: false,
|
|
100
|
+
isWritableWithResponse: true,
|
|
101
|
+
isNotifiable: false,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
// Used for write without response
|
|
105
|
+
isWritableWithoutResponse: true,
|
|
106
|
+
isWritableWithResponse: false,
|
|
107
|
+
isNotifiable: false,
|
|
108
|
+
deviceID: "20EDD96F-7430-6E33-AB22-DD8AAB857CD4",
|
|
109
|
+
isReadable: false,
|
|
110
|
+
value: null,
|
|
111
|
+
isNotifying: false,
|
|
112
|
+
isIndicatable: false,
|
|
113
|
+
id: 105553399132064,
|
|
114
|
+
uuid: "13d63400-2c97-6004-0003-4c6564676572",
|
|
115
|
+
serviceUUID: "13d63400-2c97-6004-0000-4c6564676572",
|
|
116
|
+
serviceID: 105553179758272,
|
|
117
|
+
writeWithoutResponse: async (raw) => {
|
|
118
|
+
if (!dynamicProps.isConnected)
|
|
119
|
+
throw new Error("Device is not connected");
|
|
120
|
+
|
|
121
|
+
const hex = Buffer.from(raw, "base64").toString("hex");
|
|
122
|
+
let value: Buffer;
|
|
123
|
+
|
|
124
|
+
switch (hex) {
|
|
125
|
+
// MTU handshake
|
|
126
|
+
case "0800000000":
|
|
127
|
+
value = Buffer.from("080000000199", "hex");
|
|
128
|
+
break;
|
|
129
|
+
// getAppAndVersion - returning BOLOS on 1.0.0-rc9
|
|
130
|
+
case "0500000005b010000000":
|
|
131
|
+
value = Buffer.from(
|
|
132
|
+
"05000000130105424f4c4f5309312e302e302d7263399000",
|
|
133
|
+
"hex"
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
// just used for a non resolving apdu
|
|
137
|
+
case "0500000005b020000000":
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
callbacks?.onDeviceResponse(null, {
|
|
140
|
+
value: Buffer.from("05000000029000", "hex"),
|
|
141
|
+
});
|
|
142
|
+
}, 600);
|
|
143
|
+
return; // Called after a delay to give time for the disconnect
|
|
144
|
+
default:
|
|
145
|
+
throw new Error("some generic failure");
|
|
146
|
+
}
|
|
147
|
+
// Introduce some logic to actually respond.
|
|
148
|
+
callbacks?.onDeviceResponse(null, {
|
|
149
|
+
value,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
throw Error("Generic mocked error");
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("BleTransport connectivity test coverage", () => {
|
|
165
|
+
const deviceId = "20EDD96F-7430-6E33-AB22-DD8AAB857CD4";
|
|
166
|
+
|
|
167
|
+
describe("Device available and already paired", () => {
|
|
168
|
+
it("should find the device, connect, negotiate MTU", async () => {
|
|
169
|
+
const transport = await BleTransport.open(deviceId);
|
|
170
|
+
expect(transport.device.isConnected()).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should be disconnectable, and cleanup", async () => {
|
|
174
|
+
const transport = await BleTransport.open(deviceId);
|
|
175
|
+
await BleTransport.disconnect(deviceId);
|
|
176
|
+
expect(transport.isConnected).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should disconnect in 500ms (5s default) after calling close", async () => {
|
|
180
|
+
const transport = await BleTransport.open(deviceId);
|
|
181
|
+
expect(transport.isConnected).toBe(true);
|
|
182
|
+
|
|
183
|
+
BleTransport.disconnectTimeoutMs = 500;
|
|
184
|
+
await transport.close();
|
|
185
|
+
|
|
186
|
+
// Expect the timeout for disconnection to be set
|
|
187
|
+
expect(transport.disconnectTimeout).not.toBe(undefined);
|
|
188
|
+
let resolve;
|
|
189
|
+
|
|
190
|
+
transport.on("disconnect", () => {
|
|
191
|
+
resolve();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return await new Promise((_resolve, _reject) => {
|
|
195
|
+
resolve = _resolve;
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should cancel disconnect if new connection is made", async () => {
|
|
200
|
+
const transport = await BleTransport.open(deviceId);
|
|
201
|
+
expect(transport.isConnected).toBe(true);
|
|
202
|
+
|
|
203
|
+
BleTransport.disconnectTimeoutMs = 500;
|
|
204
|
+
await transport.close();
|
|
205
|
+
|
|
206
|
+
// Expect the timeout for disconnection to be set
|
|
207
|
+
expect(transport.disconnectTimeout).not.toBe(undefined);
|
|
208
|
+
// Nb due to the different environments, the timeout behaves differently here
|
|
209
|
+
// and I can't check against a number for it to be cleared or not.
|
|
210
|
+
expect((transport.disconnectTimeout as any)._destroyed).toBe(false);
|
|
211
|
+
await BleTransport.open(deviceId);
|
|
212
|
+
expect((transport.disconnectTimeout as any)._destroyed).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should cancel disconnect if already disconnected", async () => {
|
|
216
|
+
const transport = await BleTransport.open(deviceId);
|
|
217
|
+
expect(transport.isConnected).toBe(true);
|
|
218
|
+
|
|
219
|
+
BleTransport.disconnectTimeoutMs = 500;
|
|
220
|
+
await transport.close();
|
|
221
|
+
|
|
222
|
+
// Expect the timeout for disconnection to be set
|
|
223
|
+
expect(transport.disconnectTimeout).not.toBe(undefined);
|
|
224
|
+
// Nb due to the different environments, the timeout behaves differently here
|
|
225
|
+
// and I can't check against a number for it to be cleared or not.
|
|
226
|
+
expect((transport.disconnectTimeout as any)._destroyed).toBe(false);
|
|
227
|
+
await BleTransport.disconnect(deviceId);
|
|
228
|
+
expect((transport.disconnectTimeout as any)._destroyed).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle exchanges if all goes well", async () => {
|
|
232
|
+
const transport = await BleTransport.open(deviceId);
|
|
233
|
+
expect(transport.isConnected).toBe(true);
|
|
234
|
+
|
|
235
|
+
const response = await transport.exchange(
|
|
236
|
+
Buffer.from("b010000000", "hex")
|
|
237
|
+
);
|
|
238
|
+
expect(response.toString("hex")).toBe(
|
|
239
|
+
"0105424f4c4f5309312e302e302d7263399000"
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should throw on exchanges if disconnected", async () => {
|
|
244
|
+
const transport = await BleTransport.open(deviceId);
|
|
245
|
+
expect(transport.isConnected).toBe(true);
|
|
246
|
+
await BleTransport.disconnect(deviceId);
|
|
247
|
+
await expect(
|
|
248
|
+
transport.exchange(Buffer.from("b010000000", "hex"))
|
|
249
|
+
).rejects.toThrow("Device is not connected"); // More specific errors some day.
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should disconnect if close is called, even if pending response", (done) => {
|
|
253
|
+
// This is actually a very important test, if we have an ongoing apdu response,
|
|
254
|
+
// as in, the device never replied, but we expressed the intention of disconnecting
|
|
255
|
+
// we will give it a few seconds and then disconnect regardless. Otherwise we fall
|
|
256
|
+
// in the never ending await trap.
|
|
257
|
+
async function asyncFn() {
|
|
258
|
+
const transport = await BleTransport.open(deviceId);
|
|
259
|
+
expect(transport.isConnected).toBe(true);
|
|
260
|
+
transport.exchange(Buffer.from("b020000000", "hex"));
|
|
261
|
+
BleTransport.disconnectTimeoutMs = 500;
|
|
262
|
+
|
|
263
|
+
transport.on("disconnect", () => {
|
|
264
|
+
done(); // If this is never called, then we're still waiting.
|
|
265
|
+
});
|
|
266
|
+
await transport.close();
|
|
267
|
+
|
|
268
|
+
// Expect the timeout for disconnection to be set
|
|
269
|
+
expect(transport.disconnectTimeout).not.toBe(undefined);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
asyncFn();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|