@onekeyfe/hd-transport-web-device 1.2.0-alpha.7 → 1.2.0-alpha.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
2
+ import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
3
+ import EventEmitter from 'events';
2
4
 
3
5
  import ElectronBleTransport from '../src/electron-ble-transport';
4
6
 
@@ -9,6 +11,9 @@ const protocolV1Schema = {
9
11
  Initialize: {
10
12
  fields: {},
11
13
  },
14
+ GetFeatures: {
15
+ fields: {},
16
+ },
12
17
  Success: {
13
18
  fields: {
14
19
  message: {
@@ -21,6 +26,7 @@ const protocolV1Schema = {
21
26
  values: {
22
27
  MessageType_Initialize: 1,
23
28
  MessageType_Success: 2,
29
+ MessageType_GetFeatures: 55,
24
30
  },
25
31
  },
26
32
  },
@@ -110,7 +116,10 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
110
116
  ),
111
117
  });
112
118
 
113
- const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
119
+ const configureTransport = (
120
+ nobleBle: ReturnType<typeof createNobleBle>,
121
+ emitter?: EventEmitter
122
+ ) => {
114
123
  (global as any).window = {
115
124
  desktopApi: {
116
125
  nobleBle,
@@ -118,7 +127,7 @@ const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
118
127
  };
119
128
 
120
129
  const transport = new ElectronBleTransport();
121
- transport.init(createLogger());
130
+ transport.init(createLogger(), emitter);
122
131
  transport.configure(protocolV1Schema);
123
132
  transport.configureProtocolV2(protocolV2Schema);
124
133
  return transport;
@@ -130,6 +139,72 @@ describe('ElectronBleTransport protocol detection', () => {
130
139
  jest.clearAllMocks();
131
140
  });
132
141
 
142
+ test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
143
+ const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
144
+ const nobleBle = createNobleBle(device);
145
+ const emitter = new EventEmitter();
146
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
147
+ let disconnectHandler: ((device: { id: string; name: string | null }) => void) | undefined;
148
+ let responseSeq = 0;
149
+
150
+ nobleBle.onNotification.mockImplementation(handler => {
151
+ notificationHandler = handler;
152
+ return jest.fn();
153
+ });
154
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
155
+ disconnectHandler = handler;
156
+ return jest.fn();
157
+ });
158
+ nobleBle.write.mockImplementation(() => {
159
+ responseSeq += 1;
160
+ const response = ProtocolV2.encodeFrame(
161
+ schemas,
162
+ 'Success',
163
+ { message: 'ok' },
164
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
165
+ );
166
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
167
+ return Promise.resolve();
168
+ });
169
+
170
+ const publicConnect = jest.fn();
171
+ const publicDisconnect = jest.fn();
172
+ const transportDisconnect = jest.fn();
173
+ emitter.on('device-connect', publicConnect);
174
+ emitter.on('device-disconnect', publicDisconnect);
175
+ emitter.on('transport-device-disconnect', transportDisconnect);
176
+ const bleTransport = configureTransport(nobleBle, emitter);
177
+
178
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
179
+ disconnectHandler?.(device);
180
+
181
+ expect(publicConnect).not.toHaveBeenCalled();
182
+ expect(publicDisconnect).not.toHaveBeenCalled();
183
+ expect(transportDisconnect).toHaveBeenCalledWith({
184
+ id: device.id,
185
+ connectId: device.id,
186
+ name: device.name,
187
+ });
188
+ });
189
+
190
+ test('uses the Protocol V2 BLE writer with the Electron packet size', async () => {
191
+ const device = { id: 'chunked-pro2-id', name: 'OneKey Pro 2' };
192
+ const nobleBle = createNobleBle(device);
193
+ const bleTransport = configureTransport(nobleBle) as any;
194
+ const context = {
195
+ messageName: 'Ping',
196
+ timeoutMs: 1000,
197
+ highVolume: false,
198
+ generation: 1,
199
+ signal: new AbortController().signal,
200
+ };
201
+
202
+ await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(193), context, jest.fn());
203
+
204
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
205
+ expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([192, 1]);
206
+ });
207
+
133
208
  test('detects Protocol V2 after Protocol V1 probe timeout', async () => {
134
209
  const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' };
135
210
  const nobleBle = createNobleBle(device);
@@ -140,7 +215,6 @@ describe('ElectronBleTransport protocol detection', () => {
140
215
  { message: 'ok' },
141
216
  { router: PROTOCOL_V2_CHANNEL_BLE_UART }
142
217
  );
143
-
144
218
  nobleBle.onNotification.mockImplementation(handler => {
145
219
  notificationHandler = handler;
146
220
  return jest.fn();
@@ -168,7 +242,7 @@ describe('ElectronBleTransport protocol detection', () => {
168
242
  }
169
243
  });
170
244
 
171
- test('detects Protocol V1 when device responds to Initialize', async () => {
245
+ test('reconnects Protocol V1 with a non-destructive GetFeatures probe', async () => {
172
246
  const device = { id: 'classic-id', name: 'OneKey Classic' };
173
247
  const nobleBle = createNobleBle(device);
174
248
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
@@ -182,11 +256,12 @@ describe('ElectronBleTransport protocol detection', () => {
182
256
  return jest.fn();
183
257
  });
184
258
  nobleBle.write.mockImplementation(() => {
185
- // Respond to first write (V1 Initialize probe) with V1 Success
259
+ // The first write is the V1 GetFeatures probe; answer with a V1 Success response.
186
260
  setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
187
261
  return Promise.resolve();
188
262
  });
189
263
  const transport = configureTransport(nobleBle);
264
+ const protocolV2Writer = jest.spyOn(transport as any, 'writeProtocolV2Frame');
190
265
 
191
266
  try {
192
267
  await expect(transport.acquire({ uuid: device.id })).resolves.toEqual(
@@ -195,11 +270,100 @@ describe('ElectronBleTransport protocol detection', () => {
195
270
  })
196
271
  );
197
272
  expect(transport.getProtocolType(device.id)).toBe('V1');
273
+ await expect(transport.acquire({ uuid: device.id, expectedProtocol: 'V1' })).resolves.toEqual(
274
+ expect.objectContaining({
275
+ uuid: device.id,
276
+ })
277
+ );
278
+ expect(nobleBle.write).toHaveBeenCalledTimes(2);
279
+ expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
280
+ expect(protocolV2Writer).not.toHaveBeenCalled();
198
281
  } finally {
199
282
  await transport.release(device.id);
200
283
  }
201
284
  });
202
285
 
286
+ test('invalidates and disconnects a Protocol V1 link after a response timeout', async () => {
287
+ const device = { id: 'classic-timeout-id', name: 'OneKey Classic' };
288
+ const nobleBle = createNobleBle(device);
289
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
290
+ const v1ResponseHex = '3f23230002000000040a026f6b';
291
+ let writeCount = 0;
292
+
293
+ nobleBle.onNotification.mockImplementation(handler => {
294
+ notificationHandler = handler;
295
+ return jest.fn();
296
+ });
297
+ nobleBle.write.mockImplementation(() => {
298
+ writeCount += 1;
299
+ if (writeCount === 1) {
300
+ setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
301
+ }
302
+ return Promise.resolve();
303
+ });
304
+ const bleTransport = configureTransport(nobleBle);
305
+
306
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
307
+ await expect(
308
+ bleTransport.call(device.id, 'Initialize', {}, { timeoutMs: 5 })
309
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleTimeoutError });
310
+
311
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
312
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
313
+ expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
314
+ });
315
+
316
+ test('keeps another device V2 reader when force-cleaning a V1 call', async () => {
317
+ const device = { id: 'classic-force-clean-id', name: 'OneKey Classic' };
318
+ const nobleBle = createNobleBle(device);
319
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
320
+ const v1ResponseHex = '3f23230002000000040a026f6b';
321
+ nobleBle.onNotification.mockImplementation(handler => {
322
+ notificationHandler = handler;
323
+ return jest.fn();
324
+ });
325
+ nobleBle.write.mockImplementation(() => {
326
+ setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
327
+ return Promise.resolve();
328
+ });
329
+ const bleTransport = configureTransport(nobleBle) as any;
330
+ const activeV1Call = createDeferred<string>();
331
+ const otherDeviceReader = createDeferred<Uint8Array>();
332
+ activeV1Call.promise.catch(() => undefined);
333
+ otherDeviceReader.promise.catch(() => undefined);
334
+ bleTransport.runPromise = activeV1Call;
335
+ bleTransport.v2FramePromises.set('device-b', otherDeviceReader);
336
+
337
+ await bleTransport.acquire({
338
+ uuid: device.id,
339
+ expectedProtocol: 'V1',
340
+ forceCleanRunPromise: true,
341
+ });
342
+
343
+ expect(bleTransport.v2FramePromises.get('device-b')).toBe(otherDeviceReader);
344
+ await bleTransport.release(device.id);
345
+ });
346
+
347
+ test('rejects a pending V2 reader when its device frame state resets', async () => {
348
+ const nobleBle = createNobleBle();
349
+ const bleTransport = configureTransport(nobleBle) as any;
350
+ const reader = createDeferred<Uint8Array>();
351
+ bleTransport.v2FramePromises.set('device-a', reader);
352
+ const result = Promise.race([
353
+ reader.promise.then(
354
+ () => 'resolved',
355
+ () => 'rejected'
356
+ ),
357
+ new Promise(resolve => {
358
+ setTimeout(() => resolve('pending'), 20);
359
+ }),
360
+ ]);
361
+
362
+ bleTransport.resetProtocolV2Frames('device-a');
363
+
364
+ await expect(result).resolves.toBe('rejected');
365
+ });
366
+
203
367
  test('throws when both protocol probes fail', async () => {
204
368
  const device = { id: 'dead-device-id', name: 'Unknown Device' };
205
369
  const nobleBle = createNobleBle(device);
@@ -219,19 +383,21 @@ describe('ElectronBleTransport protocol detection', () => {
219
383
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
220
384
  const nobleBle = createNobleBle(device);
221
385
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
222
- const probeResponse = ProtocolV2.encodeFrame(
223
- schemas,
224
- 'Success',
225
- { message: 'ok' },
226
- { router: PROTOCOL_V2_CHANNEL_BLE_UART }
227
- );
228
386
 
229
387
  nobleBle.onNotification.mockImplementation(handler => {
230
388
  notificationHandler = handler;
231
389
  return jest.fn();
232
390
  });
391
+ let responseSeq = 0;
233
392
  nobleBle.write.mockImplementation(() => {
234
- setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
393
+ responseSeq += 1;
394
+ const response = ProtocolV2.encodeFrame(
395
+ schemas,
396
+ 'Success',
397
+ { message: 'ok' },
398
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
399
+ );
400
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
235
401
  return Promise.resolve();
236
402
  });
237
403
  const transport = configureTransport(nobleBle);
@@ -245,8 +411,226 @@ describe('ElectronBleTransport protocol detection', () => {
245
411
  );
246
412
  expect(nobleBle.write).toHaveBeenCalledTimes(1);
247
413
  expect(transport.getProtocolType(device.id)).toBe('V2');
414
+ await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
415
+ type: 'Success',
416
+ message: { message: 'ok' },
417
+ });
418
+ const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
419
+ Number.parseInt(hex.slice(12, 14), 16)
420
+ );
421
+ expect(sentSeqs).toEqual([1, 2]);
248
422
  } finally {
249
423
  await transport.release(device.id);
250
424
  }
251
425
  });
426
+
427
+ test('rejects the active Protocol V2 reader when pairing is rejected', async () => {
428
+ const device = { id: 'pairing-rejected-pro2-id', name: 'OneKey Pro 2' };
429
+ const nobleBle = createNobleBle(device);
430
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
431
+ let pairingRejected = false;
432
+ const probeResponse = ProtocolV2.encodeFrame(
433
+ schemas,
434
+ 'Success',
435
+ { message: 'ok' },
436
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
437
+ );
438
+
439
+ nobleBle.onNotification.mockImplementation(handler => {
440
+ notificationHandler = handler;
441
+ return jest.fn();
442
+ });
443
+ nobleBle.write.mockImplementation(() => {
444
+ setTimeout(
445
+ () =>
446
+ notificationHandler?.(
447
+ device.id,
448
+ pairingRejected ? 'PAIRING_REJECTED' : bytesToHex(probeResponse)
449
+ ),
450
+ 0
451
+ );
452
+ return Promise.resolve();
453
+ });
454
+ const transport = configureTransport(nobleBle);
455
+
456
+ try {
457
+ await transport.acquire({ uuid: device.id });
458
+ pairingRejected = true;
459
+
460
+ await expect(
461
+ transport.call(device.id, 'Ping', { message: 'pairing' }, { timeoutMs: 50 })
462
+ ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleDeviceBondedCanceled });
463
+ } finally {
464
+ await transport.release(device.id);
465
+ }
466
+ });
467
+
468
+ test('rebuilds the active link when Core acquires the same device again', async () => {
469
+ const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' };
470
+ const nobleBle = createNobleBle(device);
471
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
472
+ nobleBle.onNotification.mockImplementation(handler => {
473
+ notificationHandler = handler;
474
+ return jest.fn();
475
+ });
476
+ let responseSeq = 0;
477
+ nobleBle.write.mockImplementation(() => {
478
+ responseSeq += 1;
479
+ const sequencedResponse = ProtocolV2.encodeFrame(
480
+ schemas,
481
+ 'Success',
482
+ { message: 'ok' },
483
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
484
+ );
485
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(sequencedResponse)), 0);
486
+ return Promise.resolve();
487
+ });
488
+ const transport = configureTransport(nobleBle);
489
+
490
+ try {
491
+ await transport.acquire({ uuid: device.id });
492
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
493
+ await expect(
494
+ transport.call(device.id, 'Ping', { message: 'after-reacquire' })
495
+ ).resolves.toEqual({
496
+ type: 'Success',
497
+ message: { message: 'ok' },
498
+ });
499
+
500
+ const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
501
+ Number.parseInt(hex.slice(12, 14), 16)
502
+ );
503
+ expect(sentSeqs).toEqual([1, 2, 3]);
504
+ } finally {
505
+ await transport.release(device.id);
506
+ }
507
+ });
508
+
509
+ test('ignores a delayed disconnect event from the previous BLE connection', async () => {
510
+ const device = { id: 'delayed-disconnect-pro2-id', name: 'OneKey Pro 2' };
511
+ const nobleBle = createNobleBle(device);
512
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
513
+ const disconnectHandlers: Array<
514
+ (disconnectedDevice: { id: string; name: string | null }) => void
515
+ > = [];
516
+ nobleBle.onNotification.mockImplementation(handler => {
517
+ notificationHandler = handler;
518
+ return jest.fn();
519
+ });
520
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
521
+ disconnectHandlers.push(handler);
522
+ return jest.fn();
523
+ });
524
+ let responseSeq = 0;
525
+ nobleBle.write.mockImplementation(() => {
526
+ responseSeq += 1;
527
+ const response = ProtocolV2.encodeFrame(
528
+ schemas,
529
+ 'Success',
530
+ { message: 'ok' },
531
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
532
+ );
533
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
534
+ return Promise.resolve();
535
+ });
536
+ const bleTransport = configureTransport(nobleBle);
537
+
538
+ try {
539
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
540
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
541
+
542
+ disconnectHandlers[0]?.(device);
543
+
544
+ expect(bleTransport.getProtocolType(device.id)).toBe('V2');
545
+ await expect(
546
+ bleTransport.call(device.id, 'Ping', { message: 'after-stale-disconnect' })
547
+ ).resolves.toEqual({
548
+ type: 'Success',
549
+ message: { message: 'ok' },
550
+ });
551
+ } finally {
552
+ await bleTransport.release(device.id);
553
+ }
554
+ });
555
+
556
+ test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
557
+ const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
558
+ const nobleBle = createNobleBle(device);
559
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
560
+ nobleBle.onNotification.mockImplementation(handler => {
561
+ notificationHandler = handler;
562
+ return jest.fn();
563
+ });
564
+ let responseSeq = 0;
565
+ nobleBle.write.mockImplementation(() => {
566
+ responseSeq += 1;
567
+ const response = ProtocolV2.encodeFrame(
568
+ schemas,
569
+ 'Success',
570
+ { message: 'ok' },
571
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
572
+ );
573
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
574
+ return Promise.resolve();
575
+ });
576
+ const bleTransport = configureTransport(nobleBle);
577
+
578
+ try {
579
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
580
+ const invalidateAllLinks = jest.spyOn(
581
+ (bleTransport as any).protocolV2Links,
582
+ 'invalidateAllLinks'
583
+ );
584
+ bleTransport.configureProtocolV2(protocolV2Schema);
585
+ await new Promise<void>(resolve => {
586
+ setTimeout(resolve, 0);
587
+ });
588
+ expect(invalidateAllLinks).not.toHaveBeenCalled();
589
+ await expect(
590
+ bleTransport.call(device.id, 'Ping', { message: 'same-schema' })
591
+ ).resolves.toEqual({
592
+ type: 'Success',
593
+ message: { message: 'ok' },
594
+ });
595
+ const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
596
+ Number.parseInt(hex.slice(12, 14), 16)
597
+ );
598
+ expect(sentSeqs).toEqual([1, 2]);
599
+ } finally {
600
+ await bleTransport.release(device.id);
601
+ }
602
+ });
603
+
604
+ test('rejects oversized Protocol V2 requests before writing to Electron BLE', async () => {
605
+ const device = { id: 'oversized-frame-pro2-id', name: 'OneKey Pro 2' };
606
+ const nobleBle = createNobleBle(device);
607
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
608
+ const probeResponse = ProtocolV2.encodeFrame(
609
+ schemas,
610
+ 'Success',
611
+ { message: 'ok' },
612
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
613
+ );
614
+ nobleBle.onNotification.mockImplementation(handler => {
615
+ notificationHandler = handler;
616
+ return jest.fn();
617
+ });
618
+ nobleBle.write.mockImplementation(() => {
619
+ setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
620
+ return Promise.resolve();
621
+ });
622
+ const bleTransport = configureTransport(nobleBle);
623
+
624
+ try {
625
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
626
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
627
+
628
+ await expect(
629
+ bleTransport.call(device.id, 'Ping', { message: 'x'.repeat(2048) })
630
+ ).rejects.toThrow(/Protocol V2 frame too large for transport/);
631
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
632
+ } finally {
633
+ await bleTransport.release(device.id);
634
+ }
635
+ });
252
636
  });