@onekeyfe/hd-transport-web-device 1.2.0-alpha.99 → 1.2.1-alpha.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.
@@ -1,5 +1,5 @@
1
1
  import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
2
- import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
+ import { EBleDisconnectReason, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
3
3
  import EventEmitter from 'events';
4
4
 
5
5
  import ElectronBleTransport from '../src/electron-ble-transport';
@@ -73,8 +73,26 @@ const protocolV2Schema = {
73
73
  },
74
74
  },
75
75
  },
76
+ Failure: {
77
+ fields: {
78
+ code: {
79
+ type: 'FailureType',
80
+ id: 1,
81
+ },
82
+ message: {
83
+ type: 'string',
84
+ id: 2,
85
+ },
86
+ },
87
+ },
88
+ FailureType: {
89
+ values: {
90
+ Failure_ProcessError: 5,
91
+ },
92
+ },
76
93
  MessageType: {
77
94
  values: {
95
+ MessageType_Failure: 3,
78
96
  MessageType_ProtocolInfoRequest: 60200,
79
97
  MessageType_ProtocolInfo: 60201,
80
98
  MessageType_Ping: 60206,
@@ -134,6 +152,30 @@ const configureTransport = (
134
152
  return transport;
135
153
  };
136
154
 
155
+ /**
156
+ * Wire the mock so acquire()'s Protocol V2 probe gets an answer. Without it
157
+ * detectProtocol throws and the test never reaches the disconnect behaviour.
158
+ */
159
+ const echoProtocolV2 = (nobleBle: ReturnType<typeof createNobleBle>, deviceId: string) => {
160
+ let notificationHandler: ((id: string, data: string) => void) | undefined;
161
+ nobleBle.onNotification.mockImplementation(handler => {
162
+ notificationHandler = handler;
163
+ return jest.fn();
164
+ });
165
+ let responseSeq = 0;
166
+ nobleBle.write.mockImplementation(() => {
167
+ responseSeq += 1;
168
+ const response = ProtocolV2.encodeFrame(
169
+ schemas,
170
+ 'Success',
171
+ { message: 'ok' },
172
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
173
+ );
174
+ setTimeout(() => notificationHandler?.(deviceId, bytesToHex(response)), 0);
175
+ return Promise.resolve();
176
+ });
177
+ };
178
+
137
179
  describe('ElectronBleTransport protocol detection', () => {
138
180
  afterEach(() => {
139
181
  delete (global as any).window;
@@ -294,12 +336,16 @@ describe('ElectronBleTransport protocol detection', () => {
294
336
  })
295
337
  );
296
338
  expect(transport.getProtocolType(device.id)).toBe('V2');
339
+ expect(nobleBle.connect).toHaveBeenCalledTimes(1);
340
+ expect(nobleBle.subscribe).toHaveBeenCalledTimes(1);
341
+ expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
342
+ expect(nobleBle.disconnect).not.toHaveBeenCalled();
297
343
  } finally {
298
344
  await transport.release(device.id);
299
345
  }
300
346
  });
301
347
 
302
- test('reconnects Protocol V1 with a non-destructive GetFeatures probe', async () => {
348
+ test('reconnects a declared Protocol V1 device without probing again', async () => {
303
349
  const device = { id: 'classic-id', name: 'OneKey Classic' };
304
350
  const nobleBle = createNobleBle(device);
305
351
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
@@ -332,7 +378,10 @@ describe('ElectronBleTransport protocol detection', () => {
332
378
  uuid: device.id,
333
379
  })
334
380
  );
335
- expect(nobleBle.write).toHaveBeenCalledTimes(2);
381
+ // The first acquire probes because the protocol is unknown; the second
382
+ // declares V1 and must send nothing at all, leaving the first frame on
383
+ // the link to Core — which is what carries the wallet session.
384
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
336
385
  expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
337
386
  expect(protocolV2Writer).not.toHaveBeenCalled();
338
387
  } finally {
@@ -345,19 +394,13 @@ describe('ElectronBleTransport protocol detection', () => {
345
394
  const nobleBle = createNobleBle(device);
346
395
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
347
396
  const v1ResponseHex = '3f23230002000000040a026f6b';
348
- let writeCount = 0;
349
-
350
397
  nobleBle.onNotification.mockImplementation(handler => {
351
398
  notificationHandler = handler;
352
399
  return jest.fn();
353
400
  });
354
- nobleBle.write.mockImplementation(() => {
355
- writeCount += 1;
356
- if (writeCount === 1) {
357
- setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
358
- }
359
- return Promise.resolve();
360
- });
401
+ // A declared V1 acquire writes nothing, so every write here belongs to the
402
+ // call under test and none of them is answered.
403
+ nobleBle.write.mockImplementation(() => Promise.resolve());
361
404
  const bleTransport = configureTransport(nobleBle);
362
405
 
363
406
  await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
@@ -436,7 +479,133 @@ describe('ElectronBleTransport protocol detection', () => {
436
479
  expect(transport.getProtocolType(device.id)).toBeUndefined();
437
480
  });
438
481
 
439
- test('probes Protocol V2 instead of trusting the Pro2 name hint', async () => {
482
+ test('surfaces Protocol V2 link disabled while the initial V1 probe is active', async () => {
483
+ const device = { id: 'usb-priority-pro2-id', name: 'OneKey Pro 2' };
484
+ const nobleBle = createNobleBle(device);
485
+ let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
486
+
487
+ nobleBle.onNotification.mockImplementation(handler => {
488
+ notificationHandler = handler;
489
+ return jest.fn();
490
+ });
491
+ nobleBle.write.mockImplementation(() => {
492
+ const response = ProtocolV2.encodeFrame(
493
+ schemas,
494
+ 'Failure',
495
+ { code: 5, message: 'link disabled' },
496
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
497
+ );
498
+ const splitAt = 4;
499
+ setTimeout(() => {
500
+ notificationHandler?.(device.id, bytesToHex(response.subarray(0, splitAt)));
501
+ notificationHandler?.(device.id, bytesToHex(response.subarray(splitAt)));
502
+ }, 0);
503
+ return Promise.resolve();
504
+ });
505
+ const transport = configureTransport(nobleBle);
506
+
507
+ await expect(transport.acquire({ uuid: device.id })).rejects.toMatchObject({
508
+ name: 'ProtocolV2LinkDisabledError',
509
+ failureCode: 'Failure_ProcessError',
510
+ firmwareMessage: 'link disabled',
511
+ });
512
+ expect(nobleBle.write).toHaveBeenCalledTimes(1);
513
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
514
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
515
+ });
516
+
517
+ test('keeps a first expected Protocol V2 probe miss retryable', async () => {
518
+ const device = { id: 'first-v2-id', name: 'OneKey Pro 2' };
519
+ const nobleBle = createNobleBle(device);
520
+ const transport = configureTransport(nobleBle);
521
+ jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
522
+
523
+ await expect(
524
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
525
+ ).rejects.toMatchObject({
526
+ errorCode: HardwareErrorCode.RuntimeError,
527
+ });
528
+
529
+ expect(nobleBle.connect).toHaveBeenCalledTimes(1);
530
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
531
+ });
532
+
533
+ test('keeps a second expected Protocol V2 probe miss retryable', async () => {
534
+ const device = { id: 'retry-pro2-id', name: 'OneKey Pro 2' };
535
+ const nobleBle = createNobleBle(device);
536
+ const transport = configureTransport(nobleBle);
537
+ jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
538
+
539
+ await expect(
540
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
541
+ ).rejects.toMatchObject({
542
+ errorCode: HardwareErrorCode.RuntimeError,
543
+ });
544
+ await expect(
545
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
546
+ ).rejects.toMatchObject({
547
+ errorCode: HardwareErrorCode.RuntimeError,
548
+ });
549
+
550
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
551
+ });
552
+
553
+ test('fails Protocol V2 acquire immediately when subscribe reports insufficient encryption', async () => {
554
+ const device = { id: 'stale-bond-pro2-id', name: 'OneKey Pro 2' };
555
+ const nobleBle = createNobleBle(device);
556
+ nobleBle.subscribe.mockRejectedValue(new Error('Encryption is insufficient'));
557
+ const transport = configureTransport(nobleBle);
558
+ const probe = jest.spyOn(transport as any, 'probeProtocolV2');
559
+
560
+ await expect(
561
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
562
+ ).rejects.toMatchObject({
563
+ errorCode: HardwareErrorCode.BleDeviceBondError,
564
+ });
565
+
566
+ expect(probe).not.toHaveBeenCalled();
567
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
568
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
569
+ });
570
+
571
+ test('keeps stale-bond subscribe mapping out of Protocol V1 acquire', async () => {
572
+ const device = { id: 'classic-v1-id', name: 'OneKey Classic' };
573
+ const nobleBle = createNobleBle(device);
574
+ nobleBle.subscribe.mockRejectedValue(new Error('Encryption is insufficient'));
575
+ const transport = configureTransport(nobleBle);
576
+
577
+ try {
578
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
579
+ throw new Error('Expected Protocol V1 acquire to fail');
580
+ } catch (error) {
581
+ expect(error).toBeInstanceOf(Error);
582
+ expect((error as Error).message).toBe('Encryption is insufficient');
583
+ expect((error as { errorCode?: unknown }).errorCode).toBeUndefined();
584
+ }
585
+ });
586
+
587
+ test('reports a stale bond when a previously confirmed Protocol V2 device stops responding', async () => {
588
+ const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
589
+ const nobleBle = createNobleBle(device);
590
+ const transport = configureTransport(nobleBle);
591
+ const probe = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
592
+
593
+ await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
594
+ await transport.release(device.id);
595
+ probe.mockResolvedValue(false);
596
+
597
+ await expect(
598
+ transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
599
+ ).rejects.toMatchObject({
600
+ errorCode: HardwareErrorCode.BleDeviceBondError,
601
+ });
602
+
603
+ expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
604
+ expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
605
+ expect(transport.getProtocolType(device.id)).toBeUndefined();
606
+ });
607
+
608
+ test('does not take a Protocol V2 hint from the BLE name', async () => {
440
609
  const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
441
610
  const nobleBle = createNobleBle(device);
442
611
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
@@ -466,15 +635,15 @@ describe('ElectronBleTransport protocol detection', () => {
466
635
  protocolType: 'V2',
467
636
  })
468
637
  );
469
- expect(nobleBle.write).toHaveBeenCalledTimes(1);
638
+ expect(nobleBle.write.mock.calls.length).toBeGreaterThan(1);
470
639
  expect(transport.getProtocolType(device.id)).toBe('V2');
471
640
  await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
472
641
  type: 'Success',
473
642
  message: { message: 'ok' },
474
643
  });
475
- const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
476
- Number.parseInt(hex.slice(12, 14), 16)
477
- );
644
+ const sentSeqs = nobleBle.write.mock.calls
645
+ .map(([, hex]) => Number.parseInt(hex.slice(12, 14), 16))
646
+ .filter(seq => seq > 0);
478
647
  expect(sentSeqs).toEqual([1, 2]);
479
648
  } finally {
480
649
  await transport.release(device.id);
@@ -554,30 +723,28 @@ describe('ElectronBleTransport protocol detection', () => {
554
723
  message: { message: 'ok' },
555
724
  });
556
725
 
557
- const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
558
- Number.parseInt(hex.slice(12, 14), 16)
559
- );
726
+ const sentSeqs = nobleBle.write.mock.calls
727
+ .map(([, hex]) => Number.parseInt(hex.slice(12, 14), 16))
728
+ .filter(seq => seq > 0);
560
729
  expect(sentSeqs).toEqual([1, 2, 3]);
561
730
  } finally {
562
731
  await transport.release(device.id);
563
732
  }
564
733
  });
565
734
 
566
- test('ignores a delayed disconnect event from the previous BLE connection', async () => {
567
- const device = { id: 'delayed-disconnect-pro2-id', name: 'OneKey Pro 2' };
735
+ test('subscribes to host disconnects once for the transport lifetime', async () => {
736
+ // The previous design registered a listener inside every acquire() and
737
+ // overwrote the cleanup entry without disposing the old one, so each
738
+ // acquire leaked a live handler and a delayed event from a superseded
739
+ // connection could tear down the current one. One transport-lifetime
740
+ // subscription removes that whole class of bug.
741
+ const device = { id: 'single-subscription-pro2-id', name: 'OneKey Pro 2' };
568
742
  const nobleBle = createNobleBle(device);
569
743
  let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
570
- const disconnectHandlers: Array<
571
- (disconnectedDevice: { id: string; name: string | null }) => void
572
- > = [];
573
744
  nobleBle.onNotification.mockImplementation(handler => {
574
745
  notificationHandler = handler;
575
746
  return jest.fn();
576
747
  });
577
- nobleBle.onDeviceDisconnected.mockImplementation(handler => {
578
- disconnectHandlers.push(handler);
579
- return jest.fn();
580
- });
581
748
  let responseSeq = 0;
582
749
  nobleBle.write.mockImplementation(() => {
583
750
  responseSeq += 1;
@@ -593,14 +760,15 @@ describe('ElectronBleTransport protocol detection', () => {
593
760
  const bleTransport = configureTransport(nobleBle);
594
761
 
595
762
  try {
763
+ expect(nobleBle.onDeviceDisconnected).toHaveBeenCalledTimes(1);
764
+
596
765
  await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
597
766
  await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
598
767
 
599
- disconnectHandlers[0]?.(device);
600
-
768
+ expect(nobleBle.onDeviceDisconnected).toHaveBeenCalledTimes(1);
601
769
  expect(bleTransport.getProtocolType(device.id)).toBe('V2');
602
770
  await expect(
603
- bleTransport.call(device.id, 'Ping', { message: 'after-stale-disconnect' })
771
+ bleTransport.call(device.id, 'Ping', { message: 'after-reacquire' })
604
772
  ).resolves.toEqual({
605
773
  type: 'Success',
606
774
  message: { message: 'ok' },
@@ -610,6 +778,95 @@ describe('ElectronBleTransport protocol detection', () => {
610
778
  }
611
779
  });
612
780
 
781
+ test('reports a device that drops after a logical release (OK-60486)', async () => {
782
+ // A logical release keeps the native link alive for the keep-alive window.
783
+ // A drop during that window must still reach consumers, or the UI keeps
784
+ // showing the device as connected forever.
785
+ const device = { id: 'idle-drop-pro2-id', name: 'OneKey Pro 2' };
786
+ const nobleBle = createNobleBle(device);
787
+ const emitter = new EventEmitter();
788
+ let disconnectHandler:
789
+ | ((d: { id: string; name: string; reason?: EBleDisconnectReason }) => void)
790
+ | undefined;
791
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
792
+ disconnectHandler = handler;
793
+ return jest.fn();
794
+ });
795
+ echoProtocolV2(nobleBle, device.id);
796
+ const transportDisconnect = jest.fn();
797
+ emitter.on('transport-device-disconnect', transportDisconnect);
798
+ const bleTransport = configureTransport(nobleBle, emitter);
799
+
800
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
801
+ await bleTransport.release(device.id);
802
+
803
+ disconnectHandler?.({ ...device, reason: EBleDisconnectReason.DeviceDisconnected });
804
+
805
+ expect(transportDisconnect).toHaveBeenCalledWith({
806
+ id: device.id,
807
+ connectId: device.id,
808
+ name: device.name,
809
+ });
810
+ });
811
+
812
+ test('reports an idle keep-alive release as a device disconnect', async () => {
813
+ // The main process reclaims idle links on its own timer. That is still a
814
+ // closed link, and consumers track link liveness, so it is reported like
815
+ // any other drop. Nothing reconnects on its own, so the state settles once
816
+ // until the user acts.
817
+ const device = { id: 'keep-alive-pro2-id', name: 'OneKey Pro 2' };
818
+ const nobleBle = createNobleBle(device);
819
+ const emitter = new EventEmitter();
820
+ let disconnectHandler:
821
+ | ((d: { id: string; name: string; reason?: EBleDisconnectReason }) => void)
822
+ | undefined;
823
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
824
+ disconnectHandler = handler;
825
+ return jest.fn();
826
+ });
827
+ echoProtocolV2(nobleBle, device.id);
828
+ const transportDisconnect = jest.fn();
829
+ emitter.on('transport-device-disconnect', transportDisconnect);
830
+ const bleTransport = configureTransport(nobleBle, emitter);
831
+
832
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
833
+ disconnectHandler?.({ ...device, reason: EBleDisconnectReason.IdleKeepAlive });
834
+
835
+ expect(transportDisconnect).toHaveBeenCalledWith({
836
+ id: device.id,
837
+ connectId: device.id,
838
+ name: device.name,
839
+ });
840
+ // The link really is gone, so cached link state must be dropped too.
841
+ expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
842
+ });
843
+
844
+ test('treats a disconnect with no reason as a real device drop', async () => {
845
+ // An older host bridge does not send `reason`; defaulting to "device left"
846
+ // preserves the pre-existing behaviour rather than silently ignoring it.
847
+ const device = { id: 'legacy-host-pro2-id', name: 'OneKey Pro 2' };
848
+ const nobleBle = createNobleBle(device);
849
+ const emitter = new EventEmitter();
850
+ let disconnectHandler: ((d: { id: string; name: string }) => void) | undefined;
851
+ nobleBle.onDeviceDisconnected.mockImplementation(handler => {
852
+ disconnectHandler = handler;
853
+ return jest.fn();
854
+ });
855
+ echoProtocolV2(nobleBle, device.id);
856
+ const transportDisconnect = jest.fn();
857
+ emitter.on('transport-device-disconnect', transportDisconnect);
858
+ const bleTransport = configureTransport(nobleBle, emitter);
859
+
860
+ await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
861
+ disconnectHandler?.(device);
862
+
863
+ expect(transportDisconnect).toHaveBeenCalledWith({
864
+ id: device.id,
865
+ connectId: device.id,
866
+ name: device.name,
867
+ });
868
+ });
869
+
613
870
  test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
614
871
  const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
615
872
  const nobleBle = createNobleBle(device);