@onekeyfe/hd-core 1.2.2-alpha.103 → 1.2.2-alpha.104

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,9 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DeviceSessionErrorCode } from '@onekeyfe/hd-transport';
2
3
 
4
+ import { DataManager } from '../src/data-manager';
5
+ import TransportManager from '../src/data-manager/TransportManager';
6
+ import { Device } from '../src/device/Device';
3
7
  import { DeviceCommands } from '../src/device/DeviceCommands';
4
8
  import { DEVICE } from '../src/events';
5
9
  import { LoggerNames, getLogger } from '../src/utils';
@@ -807,6 +811,255 @@ describe('DeviceCommands Protocol V2 interactive response compatibility', () =>
807
811
  });
808
812
  });
809
813
 
814
+ describe('DeviceCommands Protocol V2 session busy retry', () => {
815
+ const busy = {
816
+ type: 'Failure',
817
+ message: {
818
+ code: 'Failure_ProcessError',
819
+ subcode: DeviceSessionErrorCode.DeviceSessionError_Busy,
820
+ message: 'Another flow in progress',
821
+ },
822
+ } as const;
823
+ const success = { type: 'Success', message: {} } as const;
824
+ const flushCalls = () =>
825
+ new Promise<void>(resolve => {
826
+ setImmediate(resolve);
827
+ });
828
+
829
+ beforeEach(() => {
830
+ jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
831
+ });
832
+
833
+ afterEach(() => {
834
+ jest.useRealTimers();
835
+ jest.restoreAllMocks();
836
+ });
837
+
838
+ const setup = () => {
839
+ const commands = createCommands();
840
+ commands.device.wasInterruptedByUser = jest.fn(() => false);
841
+ commands.mainId = 'main-id';
842
+ const call = jest.fn();
843
+ commands.transport = { call } as DeviceCommands['transport'];
844
+ return { commands, call };
845
+ };
846
+
847
+ it.each(['DeviceSessionGet', 'DeviceSessionAskPin', 'DeviceSessionAskPassphrase'] as const)(
848
+ 'retries %s busy responses at 100 ms intervals without matching their message',
849
+ async request => {
850
+ const { commands, call } = setup();
851
+ const response =
852
+ request === 'DeviceSessionGet' ? { type: 'DeviceSession' as const, message: {} } : success;
853
+ call
854
+ .mockResolvedValueOnce(busy)
855
+ .mockResolvedValueOnce({
856
+ type: 'Failure',
857
+ message: { ...busy.message, message: 'Passphrase entry busy' },
858
+ })
859
+ .mockResolvedValueOnce({
860
+ type: 'Failure',
861
+ message: { ...busy.message, message: 'Passphrase confirm busy' },
862
+ })
863
+ .mockResolvedValue(response);
864
+ const payload = request === 'DeviceSessionAskPassphrase' ? { on_device: true } : {};
865
+ const result = commands.typedCall(request, response.type, payload).catch(error => error);
866
+ await flushCalls();
867
+
868
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
869
+ jest.advanceTimersByTime(99);
870
+ await flushCalls();
871
+ expect(call).toHaveBeenCalledTimes(attempt);
872
+ jest.advanceTimersByTime(1);
873
+ await flushCalls();
874
+ expect(call).toHaveBeenCalledTimes(attempt + 1);
875
+ }
876
+
877
+ await expect(result).resolves.toEqual(response);
878
+ expect(call.mock.calls).toEqual(
879
+ Array.from({ length: 4 }, () => [
880
+ commands.mainId,
881
+ request,
882
+ payload,
883
+ { expectedTypes: [response.type] },
884
+ ])
885
+ );
886
+ expect(jest.getTimerCount()).toBe(0);
887
+ }
888
+ );
889
+
890
+ it('preserves the final busy error after three retries', async () => {
891
+ const { commands, call } = setup();
892
+ call.mockResolvedValue(busy);
893
+ const result = commands.typedCall('DeviceSessionGet', 'DeviceSession').catch(error => error);
894
+ await flushCalls();
895
+ for (let retry = 0; retry < 3; retry += 1) {
896
+ jest.advanceTimersByTime(100);
897
+ await flushCalls();
898
+ }
899
+
900
+ expect(call).toHaveBeenCalledTimes(4);
901
+ await expect(result).resolves.toMatchObject({
902
+ errorCode: HardwareErrorCode.DeviceBusy,
903
+ params: {
904
+ failureCode: 'Failure_ProcessError',
905
+ subcode: DeviceSessionErrorCode.DeviceSessionError_Busy,
906
+ firmwareMessage: 'Another flow in progress',
907
+ },
908
+ });
909
+ expect(jest.getTimerCount()).toBe(0);
910
+ });
911
+
912
+ it('does not delay a successful session request', async () => {
913
+ const { commands, call } = setup();
914
+ call.mockResolvedValue(success);
915
+ await expect(commands.typedCall('DeviceSessionAskPin', 'Success')).resolves.toEqual(success);
916
+ expect(call).toHaveBeenCalledTimes(1);
917
+ expect(jest.getTimerCount()).toBe(0);
918
+ });
919
+
920
+ it.each([
921
+ ['DeviceSessionAskPin', 'Failure_ProcessError', 0],
922
+ [
923
+ 'DeviceSessionGet',
924
+ 'Failure_ProcessError',
925
+ DeviceSessionErrorCode.DeviceSessionError_InvalidSession,
926
+ ],
927
+ [
928
+ 'DeviceSessionAskPassphrase',
929
+ 'Failure_ProcessError',
930
+ DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
931
+ ],
932
+ ['DeviceSessionGet', 'Failure_DataError', DeviceSessionErrorCode.DeviceSessionError_Busy],
933
+ ['DeviceSettingsSet', 'Failure_ProcessError', 5],
934
+ ['EthereumSignTx', 'Failure_ProcessError', 5],
935
+ ] as const)('does not retry %s with %s subcode %s', async (request, code, subcode) => {
936
+ const { commands, call } = setup();
937
+ call.mockResolvedValue({ type: 'Failure', message: { code, subcode, message: 'Busy' } });
938
+ await expect(commands.typedCall(request, 'Success')).rejects.toBeInstanceOf(Error);
939
+ expect(call).toHaveBeenCalledTimes(1);
940
+ expect(jest.getTimerCount()).toBe(0);
941
+ });
942
+
943
+ it('does not retry Protocol V1 busy responses', async () => {
944
+ const { commands, call } = setup();
945
+ commands.device.isProtocolV2 = () => false;
946
+ call.mockResolvedValue(busy);
947
+ await expect(commands.typedCall('DeviceSessionGet', 'DeviceSession')).rejects.toMatchObject({
948
+ errorCode: HardwareErrorCode.DeviceBusy,
949
+ });
950
+ expect(call).toHaveBeenCalledTimes(1);
951
+ expect(jest.getTimerCount()).toBe(0);
952
+ });
953
+
954
+ it('stops retrying when the next attempt fails at the transport layer', async () => {
955
+ const { commands, call } = setup();
956
+ const error = ERRORS.TypedError(HardwareErrorCode.BridgeDeviceDisconnected);
957
+ call.mockResolvedValueOnce(busy).mockRejectedValue(error);
958
+ const result = commands.typedCall('DeviceSessionGet', 'DeviceSession').catch(err => err);
959
+ await flushCalls();
960
+ jest.advanceTimersByTime(100);
961
+ await flushCalls();
962
+ await expect(result).resolves.toBe(error);
963
+ expect(call).toHaveBeenCalledTimes(2);
964
+ expect(jest.getTimerCount()).toBe(0);
965
+ });
966
+
967
+ it('stops a superseded retry when a React Native run reuses the commands', async () => {
968
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
969
+ const call = jest.fn().mockResolvedValueOnce(busy).mockResolvedValueOnce(busy);
970
+ call.mockResolvedValue(success);
971
+ const cancel = jest.fn();
972
+ jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
973
+ call,
974
+ cancel,
975
+ } as DeviceCommands['transport']);
976
+ const device = Device.fromDescriptor({
977
+ id: 'ble-device',
978
+ path: 'ble-device',
979
+ name: 'OneKey Test',
980
+ debug: false,
981
+ commType: 'ble',
982
+ protocolType: 'V2',
983
+ });
984
+ const commands = new DeviceCommands(device, 'ble-device');
985
+ device.commands = commands;
986
+ const release = jest.spyOn(device, 'release');
987
+ const continued = jest.fn();
988
+ let staleCallResult: Promise<unknown> | undefined;
989
+ const staleRun = device
990
+ .run(
991
+ async () => {
992
+ const operation = commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
993
+ on_device: true,
994
+ });
995
+ staleCallResult = operation.catch(error => error);
996
+ await operation;
997
+ continued();
998
+ },
999
+ { keepSession: true }
1000
+ )
1001
+ .catch(error => error);
1002
+ await flushCalls();
1003
+
1004
+ const nextRun = device.run(
1005
+ async () => {
1006
+ await commands.typedCall('DeviceSessionAskPin', 'Success');
1007
+ },
1008
+ { keepSession: true }
1009
+ );
1010
+ await flushCalls();
1011
+ expect(device.commands).toBe(commands);
1012
+ expect(commands.disposed).toBe(false);
1013
+ expect(cancel).toHaveBeenCalledTimes(1);
1014
+ expect(call).toHaveBeenCalledTimes(2);
1015
+
1016
+ jest.advanceTimersByTime(100);
1017
+ await flushCalls();
1018
+
1019
+ await expect(staleRun).resolves.toMatchObject({
1020
+ errorCode: HardwareErrorCode.DeviceInterruptedFromOutside,
1021
+ });
1022
+ await expect(staleCallResult).resolves.toMatchObject({
1023
+ errorCode: HardwareErrorCode.RuntimeError,
1024
+ });
1025
+ await expect(nextRun).resolves.toBeUndefined();
1026
+ expect(call.mock.calls.map(([, request]) => request)).toEqual([
1027
+ 'DeviceSessionAskPassphrase',
1028
+ 'DeviceSessionAskPin',
1029
+ 'DeviceSessionAskPin',
1030
+ ]);
1031
+ expect(continued).not.toHaveBeenCalled();
1032
+ expect(release).not.toHaveBeenCalled();
1033
+ expect(jest.getTimerCount()).toBe(0);
1034
+ });
1035
+
1036
+ it.each(['cancelled', 'disposed'] as const)(
1037
+ 'does not resend when %s during backoff',
1038
+ async state => {
1039
+ const { commands, call } = setup();
1040
+ call.mockResolvedValueOnce(busy).mockResolvedValue(success);
1041
+ const result = commands.typedCall('DeviceSessionAskPin', 'Success').catch(error => error);
1042
+ await flushCalls();
1043
+ if (state === 'cancelled') {
1044
+ commands.device.wasInterruptedByUser = () => true;
1045
+ } else {
1046
+ commands.disposed = true;
1047
+ }
1048
+ jest.advanceTimersByTime(100);
1049
+ await flushCalls();
1050
+
1051
+ await expect(result).resolves.toMatchObject({
1052
+ errorCode:
1053
+ state === 'cancelled'
1054
+ ? HardwareErrorCode.DeviceInterruptedFromUser
1055
+ : HardwareErrorCode.RuntimeError,
1056
+ });
1057
+ expect(call).toHaveBeenCalledTimes(1);
1058
+ expect(jest.getTimerCount()).toBe(0);
1059
+ }
1060
+ );
1061
+ });
1062
+
810
1063
  describe('DeviceCommands cancellation', () => {
811
1064
  it('stops waiting after the bounded cancellation timeout', async () => {
812
1065
  jest.useFakeTimers({ doNotFake: ['performance'] });
@@ -62,6 +62,7 @@ export declare class DeviceCommands {
62
62
  transport: Transport;
63
63
  mainId: string;
64
64
  disposed: boolean;
65
+ private disposalToken?;
65
66
  callPromise?: Promise<DefaultMessageResponse>;
66
67
  constructor(device: Device, mainId: string);
67
68
  dispose(_cancelRequest: boolean): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceCommands.d.ts","sourceRoot":"","sources":["../../src/device/DeviceCommands.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DeviceCommands.d.ts","sourceRoot":"","sources":["../../src/device/DeviceCommands.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -118,6 +118,7 @@ declare class DeviceCommands {
118
118
  transport: Transport;
119
119
  mainId: string;
120
120
  disposed: boolean;
121
+ private disposalToken?;
121
122
  callPromise?: Promise<DefaultMessageResponse>;
122
123
  constructor(device: Device, mainId: string);
123
124
  dispose(_cancelRequest: boolean): Promise<void>;
package/dist/index.js CHANGED
@@ -43912,6 +43912,7 @@ class DeviceCommands {
43912
43912
  var _a, _b;
43913
43913
  return __awaiter(this, void 0, void 0, function* () {
43914
43914
  this.disposed = true;
43915
+ this.disposalToken = {};
43915
43916
  yield ((_b = (_a = this.transport).cancel) === null || _b === void 0 ? void 0 : _b.call(_a));
43916
43917
  });
43917
43918
  }
@@ -44085,7 +44086,24 @@ class DeviceCommands {
44085
44086
  }
44086
44087
  _commonCall(type, msg, options) {
44087
44088
  return __awaiter(this, void 0, void 0, function* () {
44088
- const resp = yield this.call(type, msg, options);
44089
+ const { disposalToken } = this;
44090
+ let resp = yield this.call(type, msg, options);
44091
+ for (let retry = 0; retry < 3 &&
44092
+ this.device.isProtocolV2() &&
44093
+ DEVICE_SESSION_CALLS.has(type) &&
44094
+ resp.type === 'Failure' &&
44095
+ resp.message.code === 'Failure_ProcessError' &&
44096
+ resp.message.subcode === hdTransport.DeviceSessionErrorCode.DeviceSessionError_Busy; retry += 1) {
44097
+ yield wait(100);
44098
+ if (this.device.wasInterruptedByUser()) {
44099
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromUser);
44100
+ }
44101
+ this.checkDisposed();
44102
+ if (this.disposalToken !== disposalToken) {
44103
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'DeviceCommands lifecycle changed');
44104
+ }
44105
+ resp = yield this.call(type, msg, options);
44106
+ }
44089
44107
  return this._filterCommonTypes(resp, type, options);
44090
44108
  });
44091
44109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.2-alpha.103",
3
+ "version": "1.2.2-alpha.104",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.2-alpha.103",
29
- "@onekeyfe/hd-transport": "1.2.2-alpha.103",
28
+ "@onekeyfe/hd-shared": "1.2.2-alpha.104",
29
+ "@onekeyfe/hd-transport": "1.2.2-alpha.104",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "buffer": "^6.0.3",
@@ -46,5 +46,5 @@
46
46
  "@types/w3c-web-usb": "^1.0.10",
47
47
  "@types/web-bluetooth": "^0.0.21"
48
48
  },
49
- "gitHead": "cd5aaa8a42a61c93f28b7d4931081fb7cd652e26"
49
+ "gitHead": "67835939d9f779ffd50cd8b482d8c5b53bd580bd"
50
50
  }
@@ -11,7 +11,7 @@ import {
11
11
  } from '@onekeyfe/hd-transport';
12
12
 
13
13
  import TransportManager from '../data-manager/TransportManager';
14
- import { LoggerNames, getLogger, patchFeatures } from '../utils';
14
+ import { LoggerNames, getLogger, patchFeatures, wait } from '../utils';
15
15
  import { DEVICE, type PassphraseRequestPayload } from '../events';
16
16
  import { DeviceModelToTypes } from '../types';
17
17
  import {
@@ -198,6 +198,8 @@ export class DeviceCommands {
198
198
 
199
199
  disposed: boolean;
200
200
 
201
+ private disposalToken?: object;
202
+
201
203
  callPromise?: Promise<DefaultMessageResponse>;
202
204
 
203
205
  constructor(device: Device, mainId: string) {
@@ -212,6 +214,7 @@ export class DeviceCommands {
212
214
 
213
215
  async dispose(_cancelRequest: boolean) {
214
216
  this.disposed = true;
217
+ this.disposalToken = {};
215
218
  await this.transport.cancel?.();
216
219
  }
217
220
 
@@ -439,7 +442,31 @@ export class DeviceCommands {
439
442
  msg?: DefaultMessageResponse['message'],
440
443
  options?: TransportCallOptions
441
444
  ) {
442
- const resp = await this.call(type, msg, options);
445
+ const { disposalToken } = this;
446
+ let resp = await this.call(type, msg, options);
447
+ // Session busy failures precede wallet derivation or reject an occupied flow.
448
+ // Three retries cover the 240 ms PIN dismissal animation on older V2 firmware.
449
+ for (
450
+ let retry = 0;
451
+ retry < 3 &&
452
+ this.device.isProtocolV2() &&
453
+ DEVICE_SESSION_CALLS.has(type) &&
454
+ resp.type === 'Failure' &&
455
+ (resp.message.code as string | FailureType) === 'Failure_ProcessError' &&
456
+ resp.message.subcode === DeviceSessionErrorCode.DeviceSessionError_Busy;
457
+ retry += 1
458
+ ) {
459
+ await wait(100);
460
+ if (this.device.wasInterruptedByUser()) {
461
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
462
+ }
463
+ this.checkDisposed();
464
+ if (this.disposalToken !== disposalToken) {
465
+ // React Native can revive this instance. A stale retry must not release the new run.
466
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'DeviceCommands lifecycle changed');
467
+ }
468
+ resp = await this.call(type, msg, options);
469
+ }
443
470
  return this._filterCommonTypes(resp, type, options);
444
471
  }
445
472