@onekeyfe/hd-transport-react-native 1.2.0-alpha.66 → 1.2.0-alpha.68

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.0-alpha.66",
3
+ "version": "1.2.0-alpha.68",
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-alpha.66",
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.66",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.66",
23
+ "@onekeyfe/hd-core": "1.2.0-alpha.68",
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.68",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.68",
26
26
  "@onekeyfe/react-native-ble-utils": "^0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "81629c086665bb92ce4d52087ae47f084976da79"
29
+ "gitHead": "6e8a931d08768980cfe7fa918bba447ed73e2724"
30
30
  }
@@ -0,0 +1,281 @@
1
+ import { EventEmitter } from 'events';
2
+ import { BleErrorCode } from 'react-native-ble-plx';
3
+
4
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
5
+
6
+ import ReactNativeBleTransport, {
7
+ BLE_CONNECT_TIMEOUT_MS,
8
+ BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
9
+ BLE_GATT_SETUP_TIMEOUT_MS,
10
+ } from '../index';
11
+ import protocolV1Schema from './protocolV1SchemaFixture';
12
+
13
+ jest.mock(
14
+ 'react-native',
15
+ () => ({
16
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
17
+ PermissionsAndroid: {
18
+ PERMISSIONS: {},
19
+ RESULTS: {},
20
+ request: jest.fn(),
21
+ requestMultiple: jest.fn(),
22
+ },
23
+ }),
24
+ { virtual: true }
25
+ );
26
+
27
+ jest.mock('react-native-ble-plx', () => ({
28
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
29
+ BleError: Error,
30
+ BleErrorCode: {
31
+ DeviceDisconnected: 201,
32
+ OperationStartFailed: 601,
33
+ DeviceMTUChangeFailed: 401,
34
+ OperationCancelled: 2,
35
+ OperationTimedOut: 3,
36
+ DeviceAlreadyConnected: 203,
37
+ },
38
+ BleManager: jest.fn(),
39
+ ScanMode: { LowLatency: 2 },
40
+ }));
41
+
42
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
43
+ __esModule: true,
44
+ default: {
45
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
46
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
47
+ pairDevice: jest.fn(() => Promise.resolve()),
48
+ },
49
+ }));
50
+
51
+ const UUID = 'stalled-connect-device';
52
+
53
+ const flush = () =>
54
+ new Promise(resolve => {
55
+ setImmediate(resolve);
56
+ });
57
+
58
+ async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 250) {
59
+ for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
60
+ jest.advanceTimersByTime(stepMs);
61
+ // eslint-disable-next-line no-await-in-loop
62
+ await flush();
63
+ if (settled()) return;
64
+ }
65
+ throw new Error(`fake timers exhausted after ${totalMs}ms before the connect settled`);
66
+ }
67
+
68
+ /** A device whose native connect() never settles — the observed iOS failure mode. */
69
+ function createHarness(connectImpl: () => Promise<unknown>) {
70
+ const connect = jest.fn(connectImpl);
71
+ const writeCharacteristic = {
72
+ uuid: '00000002-0000-1000-8000-00805f9b34fb',
73
+ isWritableWithResponse: true,
74
+ };
75
+ const notifyCharacteristic = {
76
+ uuid: '00000003-0000-1000-8000-00805f9b34fb',
77
+ isNotifiable: true,
78
+ };
79
+ const device = {
80
+ id: UUID,
81
+ name: 'OneKey Classic',
82
+ localName: 'OneKey Classic',
83
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
84
+ isConnected: jest.fn(() => Promise.resolve(false)),
85
+ cancelConnection: jest.fn(() => Promise.resolve()),
86
+ connect,
87
+ discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
88
+ characteristicsForService: jest.fn(() =>
89
+ Promise.resolve([writeCharacteristic, notifyCharacteristic])
90
+ ),
91
+ services: jest.fn(() => Promise.resolve([])),
92
+ onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
93
+ };
94
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
95
+ const bleManager = {
96
+ devices: jest.fn(() => Promise.resolve([device])),
97
+ connectedDevices: jest.fn(() => Promise.resolve([])),
98
+ connectToDevice: jest.fn(connectImpl),
99
+ cancelTransaction: jest.fn(() => Promise.resolve()),
100
+ cancelDeviceConnection: jest.fn(() => Promise.resolve()),
101
+ onStateChange: jest.fn((listener: (state: string) => void) => {
102
+ // Dispatch asynchronously: subscribeBleOn wires its own cleanup after
103
+ // registering, so a synchronous callback would run before it is ready.
104
+ setImmediate(() => listener('PoweredOn'));
105
+ return { remove: jest.fn() };
106
+ }),
107
+ state: jest.fn(() => Promise.resolve('PoweredOn')),
108
+ startDeviceScan: jest.fn(),
109
+ stopDeviceScan: jest.fn(),
110
+ };
111
+ (transport as any).blePlxManager = bleManager;
112
+ transport.init(
113
+ { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
114
+ new EventEmitter()
115
+ );
116
+ transport.configure(protocolV1Schema);
117
+ return { transport, device, bleManager, connect };
118
+ }
119
+
120
+ describe('BLE connect timeout', () => {
121
+ beforeAll(() => {
122
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
123
+ });
124
+
125
+ afterAll(() => {
126
+ jest.useRealTimers();
127
+ });
128
+
129
+ afterEach(() => {
130
+ jest.clearAllTimers();
131
+ jest.restoreAllMocks();
132
+ });
133
+
134
+ test('a native connect that never settles is bounded instead of blocking forever', async () => {
135
+ // iOS applies its own connect timeout on a serial queue; when that queue is busy
136
+ // the timeout never fires and acquire() blocks until the app-level 60s timeout.
137
+ const { transport } = createHarness(
138
+ () =>
139
+ new Promise(() => {
140
+ // never settles
141
+ })
142
+ );
143
+
144
+ const errors: Array<{ errorCode?: unknown }> = [];
145
+ let settled = false;
146
+ transport.acquire({ uuid: UUID }).catch(e => {
147
+ errors.push(e);
148
+ settled = true;
149
+ });
150
+ await flush();
151
+
152
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
153
+
154
+ expect(errors).toHaveLength(1);
155
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
156
+ });
157
+
158
+ test('the connect budget leaves generous headroom over a healthy connect', () => {
159
+ // Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
160
+ // fires when the native timeout itself fails to.
161
+ expect(BLE_CONNECT_TIMEOUT_MS).toBeGreaterThanOrEqual(6000);
162
+ expect(BLE_CONNECT_TIMEOUT_MS).toBeLessThanOrEqual(12000);
163
+ });
164
+
165
+ test('a stalled connect is abandoned natively so the next attempt is not cancelled by it', async () => {
166
+ const { transport, bleManager } = createHarness(
167
+ () =>
168
+ new Promise(() => {
169
+ // never settles
170
+ })
171
+ );
172
+
173
+ let settled = false;
174
+ transport.acquire({ uuid: UUID }).catch(() => {
175
+ settled = true;
176
+ });
177
+ await flush();
178
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
179
+
180
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
181
+ });
182
+
183
+ test('repeated stalled connects recreate the BLE manager', async () => {
184
+ const { transport, bleManager } = createHarness(
185
+ () =>
186
+ new Promise(() => {
187
+ // never settles
188
+ })
189
+ );
190
+ const destroy = jest.fn();
191
+ (bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
192
+
193
+ for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
194
+ let settled = false;
195
+ transport.acquire({ uuid: UUID }).catch(() => {
196
+ settled = true;
197
+ });
198
+ // eslint-disable-next-line no-await-in-loop
199
+ await flush();
200
+ // eslint-disable-next-line no-await-in-loop
201
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
202
+ }
203
+
204
+ expect(destroy).toHaveBeenCalledTimes(1);
205
+ expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
206
+ });
207
+
208
+ test('native connect timeouts contribute to the same manager reset budget', async () => {
209
+ const { transport, bleManager } = createHarness(() => Promise.resolve());
210
+ const destroy = jest.fn();
211
+ (bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
212
+ const nativeTimeout = Object.assign(new Error('Operation timed out'), {
213
+ errorCode: BleErrorCode.OperationTimedOut,
214
+ });
215
+
216
+ for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
217
+ // eslint-disable-next-line no-await-in-loop
218
+ await expect(
219
+ (transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
220
+ ).rejects.toBe(nativeTimeout);
221
+ }
222
+
223
+ expect(destroy).toHaveBeenCalledTimes(1);
224
+ });
225
+
226
+ test('GATT discovery is bounded and abandons the native connection', async () => {
227
+ const { transport, device, bleManager } = createHarness(() => Promise.resolve());
228
+ device.discoverAllServicesAndCharacteristics.mockImplementation(
229
+ () =>
230
+ new Promise(() => {
231
+ // never settles
232
+ })
233
+ );
234
+
235
+ const errors: Array<{ errorCode?: unknown }> = [];
236
+ let settled = false;
237
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch((error: unknown) => {
238
+ errors.push(error as { errorCode?: unknown });
239
+ settled = true;
240
+ });
241
+ await flush();
242
+ await advanceUntil(() => settled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
243
+
244
+ expect(errors).toHaveLength(1);
245
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
246
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
247
+ });
248
+
249
+ test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
250
+ const { transport, device, bleManager } = createHarness(() => Promise.resolve());
251
+ let resolveAbandonedDiscovery: (() => void) | undefined;
252
+ device.discoverAllServicesAndCharacteristics
253
+ .mockImplementationOnce(
254
+ () =>
255
+ new Promise<void>(resolve => {
256
+ resolveAbandonedDiscovery = resolve;
257
+ })
258
+ )
259
+ .mockResolvedValueOnce(undefined);
260
+
261
+ let firstSettled = false;
262
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch(() => {
263
+ firstSettled = true;
264
+ });
265
+ await flush();
266
+ await advanceUntil(() => firstSettled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
267
+
268
+ await expect(
269
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device)
270
+ ).resolves.toMatchObject({
271
+ writeCharacteristic: expect.any(Object),
272
+ notifyCharacteristic: expect.any(Object),
273
+ });
274
+
275
+ resolveAbandonedDiscovery?.();
276
+ await flush();
277
+
278
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
279
+ expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
280
+ });
281
+ });
@@ -0,0 +1,132 @@
1
+ import ReactNativeBleTransport, { PROTOCOL_REPROBE_FALLBACK_ATTEMPTS } from '../index';
2
+ import protocolV1Schema from './protocolV1SchemaFixture';
3
+
4
+ jest.mock(
5
+ 'react-native',
6
+ () => ({
7
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
8
+ PermissionsAndroid: {
9
+ PERMISSIONS: {},
10
+ RESULTS: {},
11
+ request: jest.fn(),
12
+ requestMultiple: jest.fn(),
13
+ },
14
+ }),
15
+ { virtual: true }
16
+ );
17
+
18
+ jest.mock('react-native-ble-plx', () => ({
19
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
20
+ BleError: Error,
21
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
22
+ BleManager: jest.fn(),
23
+ ScanMode: { LowLatency: 2 },
24
+ }));
25
+
26
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
27
+ __esModule: true,
28
+ default: {
29
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
30
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
31
+ pairDevice: jest.fn(() => Promise.resolve()),
32
+ },
33
+ }));
34
+
35
+ const UUID = 'reprobe-device';
36
+
37
+ /** Drive detectProtocol with stubbed probes so we can observe which are attempted. */
38
+ function createHarness({ v1, v2 }: { v1: boolean; v2: boolean }) {
39
+ const transport = new ReactNativeBleTransport({});
40
+ transport.configure(protocolV1Schema);
41
+ const probeV1 = jest.fn(() => {
42
+ if (v1) {
43
+ (transport as any).deviceProtocol.set(UUID, 'V1');
44
+ }
45
+ return Promise.resolve(v1);
46
+ });
47
+ const probeV2 = jest.fn(() => {
48
+ if (v2) {
49
+ (transport as any).deviceProtocol.set(UUID, 'V2');
50
+ }
51
+ return Promise.resolve(v2);
52
+ });
53
+ (transport as any).probeProtocolV1 = probeV1;
54
+ (transport as any).probeProtocolV2 = probeV2;
55
+ (transport as any).resetProbeStateAfterProtocolProbe = jest.fn(() => Promise.resolve());
56
+ return { transport, probeV1, probeV2 };
57
+ }
58
+
59
+ const detect = (transport: ReactNativeBleTransport) =>
60
+ (transport as any).detectProtocol(UUID, undefined, undefined, () =>
61
+ Promise.resolve()
62
+ ) as Promise<string>;
63
+
64
+ describe('protocol re-probe after a known device goes away', () => {
65
+ test('an unknown device still probes both protocols', async () => {
66
+ const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
67
+
68
+ await expect(detect(transport)).rejects.toBeDefined();
69
+
70
+ expect(probeV1).toHaveBeenCalledTimes(1);
71
+ expect(probeV2).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ test('a device already known to speak V1 does not pay the V2 probe while it is away', async () => {
75
+ const known = createHarness({ v1: true, v2: false });
76
+ // First detection confirms V1 (this is the session that just talked to the device).
77
+ await expect(detect(known.transport)).resolves.toBe('V1');
78
+
79
+ // The device now reboots after a firmware install: V1 stops answering.
80
+ (known.transport as any).deviceProtocol.delete(UUID);
81
+ const probeV1 = jest.fn(() => Promise.resolve(false));
82
+ const probeV2 = jest.fn(() => Promise.resolve(false));
83
+ (known.transport as any).probeProtocolV1 = probeV1;
84
+ (known.transport as any).probeProtocolV2 = probeV2;
85
+
86
+ await expect(detect(known.transport)).rejects.toBeDefined();
87
+
88
+ expect(probeV1).toHaveBeenCalledTimes(1);
89
+ // The 10s Protocol V2 ping is pure waste for a device we just spoke V1 to.
90
+ expect(probeV2).not.toHaveBeenCalled();
91
+ });
92
+
93
+ test('after repeated failures it re-probes every protocol again', async () => {
94
+ const known = createHarness({ v1: true, v2: false });
95
+ await expect(detect(known.transport)).resolves.toBe('V1');
96
+
97
+ (known.transport as any).deviceProtocol.delete(UUID);
98
+ const probeV1 = jest.fn(() => Promise.resolve(false));
99
+ const probeV2 = jest.fn(() => Promise.resolve(false));
100
+ (known.transport as any).probeProtocolV1 = probeV1;
101
+ (known.transport as any).probeProtocolV2 = probeV2;
102
+
103
+ for (let attempt = 0; attempt < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS; attempt += 1) {
104
+ // eslint-disable-next-line no-await-in-loop
105
+ await expect(detect(known.transport)).rejects.toBeDefined();
106
+ }
107
+ expect(probeV2).not.toHaveBeenCalled();
108
+
109
+ // The device may genuinely have changed protocol, so the shortcut must expire.
110
+ await expect(detect(known.transport)).rejects.toBeDefined();
111
+ expect(probeV2).toHaveBeenCalled();
112
+ });
113
+
114
+ test('a successful detection clears the failure streak', async () => {
115
+ const known = createHarness({ v1: true, v2: false });
116
+ await expect(detect(known.transport)).resolves.toBe('V1');
117
+ (known.transport as any).deviceProtocol.delete(UUID);
118
+
119
+ const failingV1 = jest.fn(() => Promise.resolve(false));
120
+ (known.transport as any).probeProtocolV1 = failingV1;
121
+ await expect(detect(known.transport)).rejects.toBeDefined();
122
+
123
+ // Device comes back.
124
+ (known.transport as any).probeProtocolV1 = jest.fn(() => {
125
+ (known.transport as any).deviceProtocol.set(UUID, 'V1');
126
+ return Promise.resolve(true);
127
+ });
128
+ await expect(detect(known.transport)).resolves.toBe('V1');
129
+
130
+ expect((known.transport as any).protocolReprobeFailures.get(UUID)).toBeUndefined();
131
+ });
132
+ });
@@ -0,0 +1,39 @@
1
+ const protocolV1Schema = {
2
+ nested: {
3
+ Initialize: {
4
+ fields: {
5
+ session_id: { type: 'bytes', id: 1 },
6
+ derive_cardano: { type: 'bool', id: 3 },
7
+ },
8
+ },
9
+ GetFeatures: { fields: {} },
10
+ Success: {
11
+ fields: {
12
+ message: { type: 'string', id: 1 },
13
+ },
14
+ },
15
+ Ping: {
16
+ fields: {
17
+ message: { type: 'string', id: 1 },
18
+ button_protection: { type: 'bool', id: 2 },
19
+ },
20
+ },
21
+ FirmwareUpload: {
22
+ fields: {
23
+ payload: { rule: 'required', type: 'bytes', id: 1 },
24
+ hash: { type: 'bytes', id: 2 },
25
+ },
26
+ },
27
+ MessageType: {
28
+ values: {
29
+ MessageType_Initialize: 0,
30
+ MessageType_Ping: 1,
31
+ MessageType_Success: 2,
32
+ MessageType_FirmwareUpload: 7,
33
+ MessageType_GetFeatures: 55,
34
+ },
35
+ },
36
+ },
37
+ };
38
+
39
+ export default protocolV1Schema;
@@ -613,6 +613,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
613
613
 
614
614
  await expect(
615
615
  transport.writeProtocolV2Packet(
616
+ 'test-device',
616
617
  {
617
618
  writeCharacteristic: {
618
619
  isWritableWithResponse: true,
@@ -648,6 +649,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
648
649
 
649
650
  try {
650
651
  const call = transport.writeProtocolV2Frame(
652
+ 'test-device',
651
653
  bleTransport,
652
654
  new Uint8Array(10),
653
655
  context,
@@ -722,6 +724,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
722
724
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
723
725
 
724
726
  await transport.writeProtocolV2Frame(
727
+ 'device-uuid',
725
728
  bleTransport,
726
729
  new Uint8Array(30),
727
730
  context,
@@ -753,7 +756,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
753
756
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
754
757
 
755
758
  await expect(
756
- transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
759
+ transport.writeProtocolV2Frame(
760
+ 'device-uuid',
761
+ bleTransport,
762
+ new Uint8Array(30),
763
+ context,
764
+ jest.fn()
765
+ )
757
766
  ).rejects.toMatchObject({ errorCode: 205 });
758
767
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
759
768
  });