@onekeyfe/hd-transport-react-native 1.2.0-alpha.99 → 1.2.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/dist/bleStaleBond.d.ts +5 -0
- package/dist/bleStaleBond.d.ts.map +1 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +354 -132
- package/package.json +5 -5
- package/src/__tests__/bleStaleBond.test.ts +39 -0
- package/src/__tests__/connectTimeout.test.ts +19 -8
- package/src/__tests__/enumerate.test.ts +30 -12
- package/src/__tests__/protocolReprobe.test.ts +24 -0
- package/src/__tests__/protocolV2Link.test.ts +291 -10
- package/src/bleStaleBond.ts +61 -0
- package/src/index.ts +418 -136
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
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.0
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.0
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.0",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0",
|
|
26
26
|
"@onekeyfe/react-native-ble-utils": "^0.1.6",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "cc51df0a415bead21304ad1d8bc92b5312c91344"
|
|
30
30
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { isNativeBleStaleBondError, toBleStaleBondHardwareError } from '../bleStaleBond';
|
|
4
|
+
|
|
5
|
+
describe('native BLE stale bond mapping', () => {
|
|
6
|
+
test.each([
|
|
7
|
+
[
|
|
8
|
+
{ attErrorCode: 15, reason: 'Encryption is insufficient' },
|
|
9
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
{ attErrorCode: 5, reason: 'GATT_INSUF_AUTHENTICATION' },
|
|
13
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
14
|
+
],
|
|
15
|
+
[
|
|
16
|
+
{ iosErrorCode: 14, reason: 'Peer removed pairing information' },
|
|
17
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
{ reason: 'peer removed pairing information' },
|
|
21
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
{ message: 'PEER REMOVED PAIRING INFORMATION' },
|
|
25
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
26
|
+
],
|
|
27
|
+
])('maps %j to %s', (nativeError, errorCode) => {
|
|
28
|
+
expect(isNativeBleStaleBondError(nativeError)).toBe(true);
|
|
29
|
+
expect(toBleStaleBondHardwareError(nativeError)).toMatchObject({ errorCode });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('does not treat the generic ATT unlikely error as a stale bond', () => {
|
|
33
|
+
expect(isNativeBleStaleBondError({ attErrorCode: 14, reason: 'Unlikely error' })).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('does not treat a generic disconnect as a stale bond', () => {
|
|
37
|
+
expect(isNativeBleStaleBondError({ reason: 'Device disconnected' })).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -6,6 +6,7 @@ import ReactNativeBleTransport, {
|
|
|
6
6
|
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
7
7
|
BLE_CONNECT_TIMEOUT_MS,
|
|
8
8
|
BLE_GATT_SETUP_TIMEOUT_MS,
|
|
9
|
+
BLE_SETUP_WEDGED_MESSAGE,
|
|
9
10
|
} from '../index';
|
|
10
11
|
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
11
12
|
|
|
@@ -179,7 +180,7 @@ describe('BLE connect timeout', () => {
|
|
|
179
180
|
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
180
181
|
});
|
|
181
182
|
|
|
182
|
-
test('repeated stalled connects recreate the BLE manager', async () => {
|
|
183
|
+
test('repeated stalled connects recreate the BLE manager and stop with PollingTimeout', async () => {
|
|
183
184
|
const { transport, bleManager } = createHarness(
|
|
184
185
|
() =>
|
|
185
186
|
new Promise(() => {
|
|
@@ -188,10 +189,12 @@ describe('BLE connect timeout', () => {
|
|
|
188
189
|
);
|
|
189
190
|
const destroy = jest.fn();
|
|
190
191
|
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
192
|
+
const errors: Array<{ errorCode?: unknown; message?: unknown }> = [];
|
|
191
193
|
|
|
192
194
|
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
193
195
|
let settled = false;
|
|
194
|
-
transport.acquire({ uuid: UUID }).catch(
|
|
196
|
+
transport.acquire({ uuid: UUID }).catch(error => {
|
|
197
|
+
errors.push(error);
|
|
195
198
|
settled = true;
|
|
196
199
|
});
|
|
197
200
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -202,6 +205,11 @@ describe('BLE connect timeout', () => {
|
|
|
202
205
|
|
|
203
206
|
expect(destroy).toHaveBeenCalledTimes(1);
|
|
204
207
|
expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
|
|
208
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
209
|
+
expect(errors[1]).toMatchObject({
|
|
210
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
211
|
+
message: BLE_SETUP_WEDGED_MESSAGE,
|
|
212
|
+
});
|
|
205
213
|
});
|
|
206
214
|
|
|
207
215
|
test('a recreated BLE manager starts with fresh timeout budgets for every device', () => {
|
|
@@ -224,12 +232,15 @@ describe('BLE connect timeout', () => {
|
|
|
224
232
|
errorCode: BleErrorCode.OperationTimedOut,
|
|
225
233
|
});
|
|
226
234
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
).
|
|
232
|
-
|
|
235
|
+
await expect(
|
|
236
|
+
(transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
|
|
237
|
+
).rejects.toBe(nativeTimeout);
|
|
238
|
+
await expect(
|
|
239
|
+
(transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
|
|
240
|
+
).rejects.toMatchObject({
|
|
241
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
242
|
+
message: BLE_SETUP_WEDGED_MESSAGE,
|
|
243
|
+
});
|
|
233
244
|
|
|
234
245
|
expect(destroy).toHaveBeenCalledTimes(1);
|
|
235
246
|
});
|
|
@@ -32,20 +32,14 @@ jest.mock('../subscribeBleOn', () => ({
|
|
|
32
32
|
const ONEKEY_SERVICE_UUID = '00000001-0000-1000-8000-00805f9b34fb';
|
|
33
33
|
|
|
34
34
|
describe('ReactNativeBleTransport iOS discovery', () => {
|
|
35
|
-
test('
|
|
35
|
+
test('keeps a bonded Pro2 communication peripheral after Find My changes its name', async () => {
|
|
36
36
|
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([
|
|
37
37
|
{
|
|
38
|
-
id: '
|
|
38
|
+
id: 'wallet-peripheral',
|
|
39
39
|
name: 'Pro2 6E9E - Find My',
|
|
40
40
|
localName: null,
|
|
41
41
|
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
id: 'wallet-peripheral',
|
|
45
|
-
name: 'Pro2 6E9E',
|
|
46
|
-
localName: 'Pro2 6E9E',
|
|
47
|
-
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
48
|
-
},
|
|
49
43
|
] as never);
|
|
50
44
|
const blePlxManager = {
|
|
51
45
|
startDeviceScan: jest.fn(),
|
|
@@ -60,7 +54,7 @@ describe('ReactNativeBleTransport iOS discovery', () => {
|
|
|
60
54
|
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
61
55
|
});
|
|
62
56
|
|
|
63
|
-
test('
|
|
57
|
+
test('uses services to distinguish a Pro2 communication peripheral from Find My', async () => {
|
|
64
58
|
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
65
59
|
const blePlxManager = {
|
|
66
60
|
startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
|
|
@@ -69,12 +63,12 @@ describe('ReactNativeBleTransport iOS discovery', () => {
|
|
|
69
63
|
id: 'find-my-peripheral',
|
|
70
64
|
name: 'Pro2 6E9E - Find My',
|
|
71
65
|
localName: null,
|
|
72
|
-
serviceUUIDs: [
|
|
66
|
+
serviceUUIDs: ['0000fffd-0000-1000-8000-00805f9b34fb'],
|
|
73
67
|
});
|
|
74
68
|
listener(null, {
|
|
75
69
|
id: 'wallet-peripheral',
|
|
76
|
-
name: 'Pro2 6E9E',
|
|
77
|
-
localName:
|
|
70
|
+
name: 'Pro2 6E9E - Find My',
|
|
71
|
+
localName: null,
|
|
78
72
|
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
79
73
|
});
|
|
80
74
|
});
|
|
@@ -90,6 +84,30 @@ describe('ReactNativeBleTransport iOS discovery', () => {
|
|
|
90
84
|
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
91
85
|
});
|
|
92
86
|
|
|
87
|
+
test('keeps a service-filtered Pro2 scan result when ble-plx omits service UUIDs', async () => {
|
|
88
|
+
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
89
|
+
const blePlxManager = {
|
|
90
|
+
startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
|
|
91
|
+
queueMicrotask(() => {
|
|
92
|
+
listener(null, {
|
|
93
|
+
id: 'wallet-peripheral',
|
|
94
|
+
name: 'Pro2 6E9E - Find My',
|
|
95
|
+
localName: null,
|
|
96
|
+
serviceUUIDs: null,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}),
|
|
100
|
+
stopDeviceScan: jest.fn(),
|
|
101
|
+
};
|
|
102
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
103
|
+
transport.blePlxManager = blePlxManager as never;
|
|
104
|
+
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
105
|
+
|
|
106
|
+
const devices = await transport.enumerate();
|
|
107
|
+
|
|
108
|
+
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
109
|
+
});
|
|
110
|
+
|
|
93
111
|
test('ignores an unnamed scanned advertisement while keeping the named wallet peripheral', async () => {
|
|
94
112
|
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
95
113
|
const blePlxManager = {
|
|
@@ -61,6 +61,30 @@ const detect = (transport: ReactNativeBleTransport) =>
|
|
|
61
61
|
Promise.resolve()
|
|
62
62
|
) as Promise<string>;
|
|
63
63
|
|
|
64
|
+
describe('iOS expected-protocol detection', () => {
|
|
65
|
+
test('still probes an expected Protocol V2 so USB-priority can surface', async () => {
|
|
66
|
+
const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: true });
|
|
67
|
+
|
|
68
|
+
await expect(
|
|
69
|
+
(transport as any).detectProtocol(UUID, 'V2', undefined, () => Promise.resolve())
|
|
70
|
+
).resolves.toBe('V2');
|
|
71
|
+
|
|
72
|
+
expect(probeV2).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(probeV1).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('keeps the existing iOS shortcut for an expected Protocol V1', async () => {
|
|
77
|
+
const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
(transport as any).detectProtocol(UUID, 'V1', undefined, () => Promise.resolve())
|
|
81
|
+
).resolves.toBe('V1');
|
|
82
|
+
|
|
83
|
+
expect(probeV1).not.toHaveBeenCalled();
|
|
84
|
+
expect(probeV2).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
64
88
|
describe('protocol re-probe after a known device goes away', () => {
|
|
65
89
|
test('an unknown device still probes both protocols', async () => {
|
|
66
90
|
const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
|
|
@@ -7,6 +7,7 @@ import transportPackage, {
|
|
|
7
7
|
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
8
8
|
|
|
9
9
|
import ReactNativeBleTransport, {
|
|
10
|
+
BLE_NATIVE_TEARDOWN_TIMEOUT_MS,
|
|
10
11
|
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
11
12
|
configureProtocolV2BleTuning,
|
|
12
13
|
getFirmwareUploadWriteRetryType,
|
|
@@ -108,9 +109,11 @@ const schemas = {
|
|
|
108
109
|
const createHarness = ({
|
|
109
110
|
deviceName = 'OneKey Pro 2',
|
|
110
111
|
isWritableWithResponse = true,
|
|
112
|
+
monitorError,
|
|
111
113
|
}: {
|
|
112
114
|
deviceName?: string;
|
|
113
115
|
isWritableWithResponse?: boolean;
|
|
116
|
+
monitorError?: (Error & { reason?: string; attErrorCode?: number; iosErrorCode?: number }) | null;
|
|
114
117
|
} = {}) => {
|
|
115
118
|
const uuid = 'rn-pro2-id';
|
|
116
119
|
const sentSeqs: number[] = [];
|
|
@@ -129,6 +132,9 @@ const createHarness = ({
|
|
|
129
132
|
isNotifiable: true,
|
|
130
133
|
monitor: jest.fn(callback => {
|
|
131
134
|
notifyCallback = callback;
|
|
135
|
+
if (monitorError) {
|
|
136
|
+
queueMicrotask(() => callback(monitorError, null));
|
|
137
|
+
}
|
|
132
138
|
return { remove: jest.fn() };
|
|
133
139
|
}),
|
|
134
140
|
};
|
|
@@ -174,6 +180,8 @@ const createHarness = ({
|
|
|
174
180
|
devices: jest.fn(() => Promise.resolve([device])),
|
|
175
181
|
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
176
182
|
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
183
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
184
|
+
destroy: jest.fn(),
|
|
177
185
|
};
|
|
178
186
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
179
187
|
const emitter = new EventEmitter();
|
|
@@ -192,6 +200,7 @@ const createHarness = ({
|
|
|
192
200
|
logger,
|
|
193
201
|
uuid,
|
|
194
202
|
device,
|
|
203
|
+
bleManager,
|
|
195
204
|
sentSeqs,
|
|
196
205
|
writeCharacteristic,
|
|
197
206
|
setShouldRespond(value: boolean) {
|
|
@@ -401,7 +410,23 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
401
410
|
await transport.release(uuid, true);
|
|
402
411
|
});
|
|
403
412
|
|
|
404
|
-
test('
|
|
413
|
+
test('keeps native stale-bond write mapping out of Protocol V1 calls', async () => {
|
|
414
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness();
|
|
415
|
+
const nativeError = Object.assign(new Error('Encryption is insufficient'), {
|
|
416
|
+
attErrorCode: 15,
|
|
417
|
+
reason: 'Encryption is insufficient',
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
421
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce(nativeError);
|
|
422
|
+
|
|
423
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
|
|
424
|
+
errorCode: HardwareErrorCode.BleWriteCharacteristicError,
|
|
425
|
+
});
|
|
426
|
+
await transport.release(uuid, true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('detects Protocol V2 on iOS without using the BLE name as a protocol hint', async () => {
|
|
405
430
|
const { transport, uuid, device, sentSeqs, writeCharacteristic } = createHarness({
|
|
406
431
|
deviceName: 'Pro2 6E9E',
|
|
407
432
|
});
|
|
@@ -411,15 +436,271 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
411
436
|
protocolType: 'V2',
|
|
412
437
|
});
|
|
413
438
|
expect(device.requestMTU).toHaveBeenCalledWith(247);
|
|
414
|
-
expect(writeCharacteristic.writeWithResponse).
|
|
439
|
+
expect(writeCharacteristic.writeWithResponse.mock.calls.length).toBeGreaterThan(1);
|
|
415
440
|
|
|
416
441
|
await expect(
|
|
417
442
|
transport.call(uuid, 'Ping', { message: 'first-core-command' })
|
|
418
443
|
).resolves.toBeDefined();
|
|
419
|
-
expect(sentSeqs).toEqual([1, 2]);
|
|
444
|
+
expect(sentSeqs.filter(seq => seq > 0)).toEqual([1, 2]);
|
|
445
|
+
await transport.release(uuid, true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('physically refreshes an uncached Protocol V2 firmware install link without Ping', async () => {
|
|
449
|
+
const { transport, uuid, device, bleManager, writeCharacteristic } = createHarness();
|
|
450
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
451
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
452
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
453
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
454
|
+
|
|
455
|
+
await expect(
|
|
456
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
457
|
+
).resolves.toEqual({
|
|
458
|
+
uuid,
|
|
459
|
+
protocolType: 'V2',
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
463
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
464
|
+
expect(device.connect).toHaveBeenCalled();
|
|
465
|
+
expect(probeProtocolV2).not.toHaveBeenCalled();
|
|
466
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
467
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
468
|
+
expect(transport.getProtocolType(uuid)).toBe('V2');
|
|
469
|
+
await transport.release(uuid, true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('refreshes a cached firmware install connection instead of trusting GATT state', async () => {
|
|
473
|
+
const { transport, uuid, device, bleManager, writeCharacteristic } = createHarness();
|
|
474
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
475
|
+
|
|
476
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
477
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
478
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
479
|
+
writeCharacteristic.writeWithResponse.mockClear();
|
|
480
|
+
writeCharacteristic.writeWithoutResponse.mockClear();
|
|
481
|
+
|
|
482
|
+
await expect(
|
|
483
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
484
|
+
).resolves.toEqual({
|
|
485
|
+
uuid,
|
|
486
|
+
protocolType: 'V2',
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
490
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
491
|
+
expect(device.connect).toHaveBeenCalled();
|
|
492
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
493
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
494
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
420
495
|
await transport.release(uuid, true);
|
|
421
496
|
});
|
|
422
497
|
|
|
498
|
+
test('rejects no-probe acquire before the BLE endpoint has confirmed a protocol', async () => {
|
|
499
|
+
const { transport, uuid } = createHarness();
|
|
500
|
+
|
|
501
|
+
await expect(
|
|
502
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
503
|
+
).rejects.toThrow('previously confirmed protocol');
|
|
504
|
+
|
|
505
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('keeps confirmed V2 authorization across a BLE manager reset', async () => {
|
|
509
|
+
const { transport, uuid, device, bleManager, writeCharacteristic } = createHarness();
|
|
510
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
511
|
+
|
|
512
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
513
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
514
|
+
|
|
515
|
+
(transport as any).resetPlxManager();
|
|
516
|
+
transport.blePlxManager = bleManager as any;
|
|
517
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
518
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
519
|
+
writeCharacteristic.writeWithResponse.mockClear();
|
|
520
|
+
writeCharacteristic.writeWithoutResponse.mockClear();
|
|
521
|
+
|
|
522
|
+
await expect(
|
|
523
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
524
|
+
).resolves.toEqual({
|
|
525
|
+
uuid,
|
|
526
|
+
protocolType: 'V2',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
530
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
531
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
532
|
+
await transport.release(uuid, true);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test.each(['ios', 'android'] as const)(
|
|
536
|
+
'keeps a first expected Protocol V2 probe miss retryable on %s',
|
|
537
|
+
async platform => {
|
|
538
|
+
setPlatformOS(platform);
|
|
539
|
+
const { transport, uuid, device } = createHarness();
|
|
540
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
541
|
+
|
|
542
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
543
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
547
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
test.each(['ios', 'android'] as const)(
|
|
552
|
+
'keeps a second expected Protocol V2 probe miss retryable on %s',
|
|
553
|
+
async platform => {
|
|
554
|
+
setPlatformOS(platform);
|
|
555
|
+
const { transport, uuid, device } = createHarness();
|
|
556
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
557
|
+
|
|
558
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
559
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
560
|
+
});
|
|
561
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
562
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
566
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
test.each([
|
|
571
|
+
[
|
|
572
|
+
'Encryption is insufficient',
|
|
573
|
+
{ reason: 'Encryption is insufficient', attErrorCode: 15 },
|
|
574
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
575
|
+
],
|
|
576
|
+
[
|
|
577
|
+
'Peer removed pairing information',
|
|
578
|
+
{ reason: 'Peer removed pairing information', iosErrorCode: 14 },
|
|
579
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
580
|
+
],
|
|
581
|
+
] as const)(
|
|
582
|
+
'fails Protocol V2 acquire immediately on %s instead of waiting for Ping',
|
|
583
|
+
async (_label, nativeError, errorCode) => {
|
|
584
|
+
const { transport, uuid, device } = createHarness({
|
|
585
|
+
monitorError: Object.assign(new Error(nativeError.reason), nativeError),
|
|
586
|
+
});
|
|
587
|
+
const probe = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
588
|
+
|
|
589
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
590
|
+
errorCode,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
expect(probe).not.toHaveBeenCalled();
|
|
594
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
595
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
test.each(['ios', 'android'] as const)(
|
|
600
|
+
'keeps a confirmed Protocol V2 probe miss retryable on %s without native bond evidence',
|
|
601
|
+
async platform => {
|
|
602
|
+
setPlatformOS(platform);
|
|
603
|
+
const { transport, uuid, device } = createHarness();
|
|
604
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
605
|
+
await transport.release(uuid, true);
|
|
606
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
607
|
+
|
|
608
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
609
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
613
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
test('waits for an in-flight release before reacquiring the same device', async () => {
|
|
618
|
+
const { transport, uuid } = createHarness();
|
|
619
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
620
|
+
const releaseGate = createDeferred<void>();
|
|
621
|
+
const releaseStarted = createDeferred<void>();
|
|
622
|
+
const { protocolV2Links } = transport as any;
|
|
623
|
+
const invalidateLink = protocolV2Links.invalidateLink.bind(protocolV2Links);
|
|
624
|
+
jest
|
|
625
|
+
.spyOn(protocolV2Links, 'invalidateLink')
|
|
626
|
+
.mockImplementationOnce(async (...args: unknown[]) => {
|
|
627
|
+
releaseStarted.resolve();
|
|
628
|
+
await releaseGate.promise;
|
|
629
|
+
return invalidateLink(...args);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const release = transport.release(uuid, true);
|
|
633
|
+
await releaseStarted.promise;
|
|
634
|
+
let reacquired = false;
|
|
635
|
+
const acquire = transport.acquire({ uuid, expectedProtocol: 'V2' }).then(result => {
|
|
636
|
+
reacquired = true;
|
|
637
|
+
return result;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await Promise.resolve();
|
|
641
|
+
expect(reacquired).toBe(false);
|
|
642
|
+
|
|
643
|
+
releaseGate.resolve();
|
|
644
|
+
await release;
|
|
645
|
+
await expect(acquire).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
646
|
+
await expect(transport.call(uuid, 'Ping', { message: 'after-release' })).resolves.toMatchObject(
|
|
647
|
+
{
|
|
648
|
+
type: 'Success',
|
|
649
|
+
message: { message: 'ok' },
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test(
|
|
655
|
+
'releases the lifecycle queue when native teardown never settles',
|
|
656
|
+
async () => {
|
|
657
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
658
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
659
|
+
const otherUuid = 'rn-pro2-other-id';
|
|
660
|
+
const otherDevice = {
|
|
661
|
+
...device,
|
|
662
|
+
id: otherUuid,
|
|
663
|
+
name: 'OneKey Pro 2 Other',
|
|
664
|
+
localName: 'OneKey Pro 2 Other',
|
|
665
|
+
};
|
|
666
|
+
await (transport as any).installTransportForAcquire(otherUuid, otherDevice);
|
|
667
|
+
(transport as any).deviceProtocol.set(otherUuid, 'V2');
|
|
668
|
+
const disconnectEvents: Array<{ connectId: string }> = [];
|
|
669
|
+
transport.emitter?.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, event => {
|
|
670
|
+
disconnectEvents.push(event);
|
|
671
|
+
});
|
|
672
|
+
const stalledNativeCleanup = createDeferred<void>();
|
|
673
|
+
bleManager.cancelTransaction
|
|
674
|
+
.mockImplementationOnce(() => stalledNativeCleanup.promise)
|
|
675
|
+
.mockResolvedValue(undefined);
|
|
676
|
+
const resetPlxManager = jest.spyOn(transport as any, 'resetPlxManager');
|
|
677
|
+
const nextLifecycleOperation = jest.fn().mockResolvedValue('next-operation');
|
|
678
|
+
|
|
679
|
+
const release = transport.release(uuid, true);
|
|
680
|
+
const nextOperation = (transport as any).runLifecycleOperation(uuid, nextLifecycleOperation);
|
|
681
|
+
|
|
682
|
+
await expect(release).resolves.toBe(true);
|
|
683
|
+
await expect(nextOperation).resolves.toBe('next-operation');
|
|
684
|
+
expect(resetPlxManager).toHaveBeenCalledTimes(1);
|
|
685
|
+
expect(nextLifecycleOperation).toHaveBeenCalledTimes(1);
|
|
686
|
+
expect(disconnectEvents).toContainEqual(expect.objectContaining({ connectId: otherUuid }));
|
|
687
|
+
expect(() => (transport as any).getCachedTransport(otherUuid)).toThrow();
|
|
688
|
+
|
|
689
|
+
await (transport as any).installTransportForAcquire(uuid, device);
|
|
690
|
+
(transport as any).deviceProtocol.set(uuid, 'V2');
|
|
691
|
+
|
|
692
|
+
stalledNativeCleanup.resolve();
|
|
693
|
+
await Promise.resolve();
|
|
694
|
+
await expect(
|
|
695
|
+
transport.call(uuid, 'Ping', { message: 'after-stale-cleanup' })
|
|
696
|
+
).resolves.toMatchObject({
|
|
697
|
+
type: 'Success',
|
|
698
|
+
message: { message: 'ok' },
|
|
699
|
+
});
|
|
700
|
+
},
|
|
701
|
+
BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
|
|
702
|
+
);
|
|
703
|
+
|
|
423
704
|
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
424
705
|
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
425
706
|
const probeProtocolV1 = jest
|
|
@@ -625,7 +906,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
625
906
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
626
907
|
await transport.call(uuid, 'Ping', { message: 'first-core-command' });
|
|
627
908
|
|
|
628
|
-
expect(sentSeqs).toEqual([1]);
|
|
909
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
629
910
|
await transport.release(uuid, true);
|
|
630
911
|
});
|
|
631
912
|
|
|
@@ -660,7 +941,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
660
941
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
661
942
|
await transport.call(uuid, 'Ping', { message: 'second-generation' });
|
|
662
943
|
|
|
663
|
-
expect(sentSeqs).toEqual([1, 2]);
|
|
944
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
664
945
|
await transport.release(uuid, true);
|
|
665
946
|
});
|
|
666
947
|
|
|
@@ -670,12 +951,12 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
670
951
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
671
952
|
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
672
953
|
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
673
|
-
expect(writeCharacteristic.writeWithResponse).
|
|
954
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
674
955
|
|
|
675
956
|
await transport.call(uuid, 'DeviceInfoGet', {});
|
|
676
957
|
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
677
958
|
|
|
678
|
-
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(
|
|
959
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(3);
|
|
679
960
|
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
680
961
|
expect(releaseNative).not.toHaveBeenCalled();
|
|
681
962
|
|
|
@@ -690,7 +971,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
690
971
|
await transport.call(uuid, 'FileWrite', {});
|
|
691
972
|
await transport.call(uuid, 'FileWrite', {});
|
|
692
973
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
|
|
693
|
-
expect(writeCharacteristic.writeWithResponse).
|
|
974
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
694
975
|
expect(
|
|
695
976
|
logger.debug.mock.calls.filter(
|
|
696
977
|
([message]) =>
|
|
@@ -723,7 +1004,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
723
1004
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
724
1005
|
|
|
725
1006
|
await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
|
|
726
|
-
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(
|
|
1007
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
727
1008
|
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
728
1009
|
await transport.release(uuid, true);
|
|
729
1010
|
});
|
|
@@ -737,7 +1018,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
737
1018
|
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
738
1019
|
|
|
739
1020
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
740
|
-
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
1021
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
|
|
741
1022
|
await transport.release(uuid, true);
|
|
742
1023
|
});
|
|
743
1024
|
|