@onekeyfe/hd-core 1.0.25 → 1.0.26

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.
@@ -102,6 +102,16 @@ export class Device extends EventEmitter {
102
102
  // @ts-expect-error: strictPropertyInitialization
103
103
  commands: DeviceCommands;
104
104
 
105
+ /**
106
+ * 可取消的操作
107
+ */
108
+ private cancelableAction?: (err?: Error) => Promise<unknown>;
109
+
110
+ /**
111
+ * 设备是否被占用
112
+ */
113
+ private deviceAcquired = false;
114
+
105
115
  /**
106
116
  * 设备信息
107
117
  */
@@ -221,6 +231,7 @@ export class Device extends EventEmitter {
221
231
  );
222
232
  Log.debug('Expected session id:', this.mainId);
223
233
  }
234
+ this.deviceAcquired = true;
224
235
  this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
225
236
  if (this.commands) {
226
237
  await this.commands.dispose(false);
@@ -262,6 +273,7 @@ export class Device extends EventEmitter {
262
273
  this.needReloadDevice = true;
263
274
  }
264
275
  }
276
+ this.deviceAcquired = false;
265
277
  }
266
278
 
267
279
  getCommands() {
@@ -481,6 +493,12 @@ export class Device extends EventEmitter {
481
493
  )
482
494
  );
483
495
  }
496
+ } else if (env === 'react-native') {
497
+ // TODO: implement react-native acquire
498
+ // cancel input pin or passphrase on device request, then the following requests will report an error
499
+ if (this.commands) {
500
+ this.commands.disposed = false;
501
+ }
484
502
  }
485
503
  }
486
504
 
@@ -542,14 +560,31 @@ export class Device extends EventEmitter {
542
560
  }
543
561
 
544
562
  async interruptionFromUser() {
545
- if (this.commands) {
546
- await this.commands.dispose(true);
547
- }
563
+ const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
564
+ await this.cancelableAction?.(error);
565
+ await this.commands?.cancel();
566
+
548
567
  if (this.runPromise) {
549
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser));
568
+ this.runPromise.reject(error);
569
+ this.runPromise = null;
550
570
  }
551
571
  }
552
572
 
573
+ setCancelableAction(callback: NonNullable<typeof this.cancelableAction>) {
574
+ this.cancelableAction = (e?: Error) =>
575
+ callback(e)
576
+ .catch(e2 => {
577
+ Log.debug('cancelableAction error', e2);
578
+ })
579
+ .finally(() => {
580
+ this.clearCancelableAction();
581
+ });
582
+ }
583
+
584
+ clearCancelableAction() {
585
+ this.cancelableAction = undefined;
586
+ }
587
+
553
588
  getMode() {
554
589
  if (this.features?.bootloader_mode) {
555
590
  // bootloader mode
@@ -584,6 +619,14 @@ export class Device extends EventEmitter {
584
619
  return typeof this.originalDescriptor.session === 'string';
585
620
  }
586
621
 
622
+ hasDeviceAcquire() {
623
+ const env = DataManager.getSettings('env');
624
+ if (DataManager.isBleConnect(env)) {
625
+ return this.deviceAcquired;
626
+ }
627
+ return this.isUsed() && this.deviceAcquired;
628
+ }
629
+
587
630
  isUsedHere() {
588
631
  const env = DataManager.getSettings('env');
589
632
  if (DataManager.isBleConnect(env)) {
@@ -2,9 +2,16 @@ import type { Transport, Messages, FailureType } from '@onekeyfe/hd-transport';
2
2
  import { ERRORS, HardwareError, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
3
  import TransportManager from '../data-manager/TransportManager';
4
4
  import DataManager from '../data-manager/DataManager';
5
- import { patchFeatures, getLogger, LoggerNames } from '../utils';
5
+ import { patchFeatures, getLogger, LoggerNames, getDeviceType } from '../utils';
6
6
  import type { Device } from './Device';
7
7
  import { DEVICE } from '../events';
8
+ import { DeviceModelToTypes } from '../types';
9
+
10
+ export type PassphrasePromptResponse = {
11
+ passphrase?: string;
12
+ passphraseOnDevice?: boolean;
13
+ cache?: boolean;
14
+ };
8
15
 
9
16
  type MessageType = Messages.MessageType;
10
17
  type MessageKey = keyof MessageType;
@@ -17,12 +24,6 @@ type TypedCallResponseMap = {
17
24
  };
18
25
  export type DefaultMessageResponse = TypedCallResponseMap[keyof MessageType];
19
26
 
20
- export type PassphrasePromptResponse = {
21
- passphrase?: string;
22
- passphraseOnDevice?: boolean;
23
- cache?: boolean;
24
- };
25
-
26
27
  const assertType = (res: DefaultMessageResponse, resType: string | string[]) => {
27
28
  const splitResTypes = Array.isArray(resType) ? resType : resType.split('|');
28
29
  if (!splitResTypes.includes(res.type)) {
@@ -33,8 +34,90 @@ const assertType = (res: DefaultMessageResponse, resType: string | string[]) =>
33
34
  }
34
35
  };
35
36
 
37
+ export const cancelDeviceInPrompt = (device: Device, expectResponse = true) => {
38
+ const session = device.hasDeviceAcquire() ? device.mainId : undefined;
39
+
40
+ if (!session) {
41
+ // device disconnected or acquired by someone else
42
+ return Promise.resolve({
43
+ success: false,
44
+ error: HardwareErrorCode.RuntimeError,
45
+ payload: {
46
+ message: 'Device disconnected or acquired by someone else',
47
+ },
48
+ } as const);
49
+ }
50
+
51
+ const transport = device.commands?.transport;
52
+
53
+ if (expectResponse) {
54
+ return transport
55
+ ?.call(session, 'Cancel', {})
56
+ .then(() => ({
57
+ success: true,
58
+ error: null,
59
+ payload: {
60
+ message: 'Cancel request sent',
61
+ },
62
+ }))
63
+ .catch((error: HardwareError) => ({
64
+ success: false,
65
+ error: error.errorCode,
66
+ payload: {
67
+ message: error.message,
68
+ },
69
+ }));
70
+ }
71
+
72
+ return transport?.post(session, 'Cancel', {}).then(() => ({
73
+ success: true,
74
+ error: HardwareErrorCode.RuntimeError,
75
+ payload: {
76
+ message: 'Cancel request sent',
77
+ },
78
+ }));
79
+ };
80
+
81
+ export const cancelDeviceWithInitialize = (device: Device) => {
82
+ const session = device.hasDeviceAcquire() ? device.mainId : undefined;
83
+
84
+ if (!session) {
85
+ // device disconnected or acquired by someone else
86
+ return Promise.resolve({
87
+ success: false,
88
+ error: HardwareErrorCode.RuntimeError,
89
+ payload: {
90
+ message: 'Device disconnected or acquired by someone else',
91
+ },
92
+ } as const);
93
+ }
94
+
95
+ const transport = device.commands?.transport;
96
+
97
+ return transport
98
+ ?.call(session, 'Initialize', {})
99
+ .then(() => ({
100
+ success: true,
101
+ error: null,
102
+ payload: {
103
+ message: 'Cancel request sent',
104
+ },
105
+ }))
106
+ .catch((error: HardwareError) => ({
107
+ success: false,
108
+ error: error.errorCode,
109
+ payload: {
110
+ message: error.message,
111
+ },
112
+ }));
113
+ };
114
+
36
115
  const Log = getLogger(LoggerNames.DeviceCommands);
37
116
 
117
+ /**
118
+ * The life cycle begins with the acquisition of the device and ends with the disposal device commands
119
+ * acquire device -> create DeviceCommands -> release device -> dispose DeviceCommands
120
+ */
38
121
  export class DeviceCommands {
39
122
  device: Device;
40
123
 
@@ -46,8 +129,6 @@ export class DeviceCommands {
46
129
 
47
130
  callPromise?: Promise<DefaultMessageResponse>;
48
131
 
49
- _cancelableRequest?: (error?: any) => void;
50
-
51
132
  constructor(device: Device, mainId: string) {
52
133
  this.device = device;
53
134
  this.mainId = mainId;
@@ -55,15 +136,75 @@ export class DeviceCommands {
55
136
  this.disposed = false;
56
137
  }
57
138
 
58
- async dispose(cancelRequest: boolean) {
139
+ async dispose(_cancelRequest: boolean) {
59
140
  this.disposed = true;
60
- if (cancelRequest && this._cancelableRequest) {
61
- this._cancelableRequest();
62
- }
63
- this._cancelableRequest = undefined;
64
141
  await this.transport.cancel?.();
65
142
  }
66
143
 
144
+ checkDisposed() {
145
+ if (this.disposed) {
146
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'DeviceCommands already disposed');
147
+ }
148
+ }
149
+
150
+ // on device input pin or passphrase, cancel the request with initialize
151
+ async cancelDeviceOnOneKeyDevice() {
152
+ const { name } = this.transport;
153
+ if (name === 'HttpTransport') {
154
+ /**
155
+ * Bridge throws "other call in progress" error.
156
+ * as workaround takeover transportSession (acquire) before sending Cancel, this will resolve previous pending call.
157
+ */
158
+ try {
159
+ await this.device.acquire();
160
+ await cancelDeviceWithInitialize(this.device);
161
+ } catch {
162
+ // ignore whatever happens
163
+ }
164
+ } else {
165
+ return cancelDeviceWithInitialize(this.device);
166
+ }
167
+ }
168
+
169
+ async cancelDevice() {
170
+ const { name } = this.transport;
171
+ if (name === 'HttpTransport') {
172
+ /**
173
+ * Bridge throws "other call in progress" error.
174
+ * as workaround takeover transportSession (acquire) before sending Cancel, this will resolve previous pending call.
175
+ */
176
+ try {
177
+ await this.device.acquire();
178
+ await cancelDeviceInPrompt(this.device, false);
179
+ } catch {
180
+ // ignore whatever happens
181
+ }
182
+ } else {
183
+ return cancelDeviceInPrompt(this.device, false);
184
+ }
185
+ }
186
+
187
+ async cancel() {
188
+ if (this.disposed) {
189
+ return;
190
+ }
191
+ this.dispose(true);
192
+ if (this.callPromise) {
193
+ try {
194
+ await Promise.all([
195
+ new Promise((_resolve, reject) =>
196
+ // eslint-disable-next-line no-promise-executor-return
197
+ setTimeout(() => reject(new Error('cancel timeout')), 10 * 1000)
198
+ ),
199
+ await this.callPromise,
200
+ ]);
201
+ } catch {
202
+ // device error
203
+ this.callPromise = undefined;
204
+ }
205
+ }
206
+ }
207
+
67
208
  // Sends an async message to the opened device.
68
209
  async call(
69
210
  type: MessageKey,
@@ -199,6 +340,7 @@ export class DeviceCommands {
199
340
  // ignore
200
341
  }
201
342
 
343
+ this.device.clearCancelableAction();
202
344
  if (res.type === 'Failure') {
203
345
  const { code, message } = res.message as {
204
346
  code?: string | FailureType;
@@ -261,6 +403,12 @@ export class DeviceCommands {
261
403
  }
262
404
 
263
405
  if (res.type === 'ButtonRequest') {
406
+ const deviceType = getDeviceType(this.device.features);
407
+ if (DeviceModelToTypes.model_mini.includes(deviceType)) {
408
+ this.device.setCancelableAction(() => this.cancelDeviceOnOneKeyDevice());
409
+ } else {
410
+ this.device.setCancelableAction(() => this.cancelDevice());
411
+ }
264
412
  if (res.message.code === 'ButtonRequest_PassphraseEntry') {
265
413
  this.device.emit(DEVICE.PASSPHRASE_ON_DEVICE, this.device);
266
414
  } else {
@@ -277,11 +425,15 @@ export class DeviceCommands {
277
425
  return this._promptPin(res.message.type).then(
278
426
  pin => {
279
427
  if (pin === '@@ONEKEY_INPUT_PIN_IN_DEVICE') {
280
- return this._commonCall('BixinPinInputOnDevice');
428
+ // only classic\1s\mini\pure
429
+ this.device.setCancelableAction(() => this.cancelDeviceOnOneKeyDevice());
430
+ return this._commonCall('BixinPinInputOnDevice').finally(() => {
431
+ this.device.clearCancelableAction();
432
+ });
281
433
  }
282
434
  return this._commonCall('PinMatrixAck', { pin });
283
435
  },
284
- () => this._commonCall('Cancel', {})
436
+ error => Promise.reject(error)
285
437
  );
286
438
  }
287
439
 
@@ -308,12 +460,31 @@ export class DeviceCommands {
308
460
 
309
461
  _promptPin(type?: Messages.PinMatrixRequestType) {
310
462
  return new Promise<string>((resolve, reject) => {
463
+ const cancelAndReject = (_error?: Error) =>
464
+ cancelDeviceInPrompt(this.device, false)
465
+ .then(onCancel => {
466
+ const error = ERRORS.TypedError(
467
+ HardwareErrorCode.ActionCancelled,
468
+ `${DEVICE.PIN} canceled`
469
+ );
470
+ // onCancel not void
471
+ if (onCancel) {
472
+ const { payload } = onCancel || {};
473
+ reject(error || new Error(payload?.message));
474
+ } else {
475
+ reject(error);
476
+ }
477
+ })
478
+ .catch(error => {
479
+ reject(error);
480
+ });
481
+
311
482
  if (this.device.listenerCount(DEVICE.PIN) > 0) {
312
- this._cancelableRequest = reject;
483
+ this.device.setCancelableAction(cancelAndReject);
313
484
  this.device.emit(DEVICE.PIN, this.device, type, (err, pin) => {
314
- this._cancelableRequest = undefined;
485
+ this.device.clearCancelableAction();
315
486
  if (err) {
316
- reject(err);
487
+ cancelAndReject(err);
317
488
  } else {
318
489
  resolve(pin);
319
490
  }
@@ -332,15 +503,34 @@ export class DeviceCommands {
332
503
 
333
504
  _promptPassphrase() {
334
505
  return new Promise<PassphrasePromptResponse>((resolve, reject) => {
506
+ const cancelAndReject = (_error?: Error) =>
507
+ cancelDeviceInPrompt(this.device, false)
508
+ .then(onCancel => {
509
+ const error = ERRORS.TypedError(
510
+ HardwareErrorCode.ActionCancelled,
511
+ `${DEVICE.PASSPHRASE} canceled`
512
+ );
513
+ // onCancel not void
514
+ if (onCancel) {
515
+ const { payload } = onCancel || {};
516
+ reject(error || new Error(payload?.message));
517
+ } else {
518
+ reject(error);
519
+ }
520
+ })
521
+ .catch(error => {
522
+ reject(error);
523
+ });
524
+
335
525
  if (this.device.listenerCount(DEVICE.PASSPHRASE) > 0) {
336
- this._cancelableRequest = reject;
526
+ this.device.setCancelableAction(cancelAndReject);
337
527
  this.device.emit(
338
528
  DEVICE.PASSPHRASE,
339
529
  this.device,
340
530
  (response: PassphrasePromptResponse, error?: Error) => {
341
- this._cancelableRequest = undefined;
531
+ this.device.clearCancelableAction();
342
532
  if (error) {
343
- reject(error);
533
+ cancelAndReject(error);
344
534
  } else {
345
535
  resolve(response);
346
536
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Ensures that since the awaited lock is obtained until unlock return from it
3
+ * is called, no other part of code can enter the same lock.
4
+ *
5
+ * Optionally, it also takes `lockId` param, in which case only actions with
6
+ * the same lock id are blocking each other.
7
+ *
8
+ * Example:
9
+ *
10
+ * ```
11
+ * const lock = getMutex();
12
+ *
13
+ * lock().then(unlock => writeToSocket('foo').finally(unlock));
14
+ * lock().then(unlock => writeToSocket('bar').finally(unlock));
15
+ * lock('differentLockId').then(unlock => writeToAnotherSocket('baz').finally(unlock));
16
+ *
17
+ * const unlock = await lock();
18
+ * await readFromSocket();
19
+ * unlock();
20
+ * ```
21
+ */
22
+ export const getMutex = () => {
23
+ // eslint-disable-next-line symbol-description
24
+ const DEFAULT_ID = Symbol();
25
+ const locks: Record<keyof any, Promise<void>> = {};
26
+
27
+ return async (lockId: keyof any = DEFAULT_ID) => {
28
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
29
+ while (locks[lockId]) {
30
+ await locks[lockId];
31
+ }
32
+ let resolve = () => {};
33
+ locks[lockId] = new Promise<void>(res => {
34
+ resolve = res;
35
+ }).finally(() => {
36
+ delete locks[lockId];
37
+ });
38
+
39
+ return resolve;
40
+ };
41
+ };
@@ -0,0 +1,25 @@
1
+ import { getMutex } from './getMutex';
2
+
3
+ /**
4
+ * Ensures that all async actions passed to the returned function are called
5
+ * immediately one after another, without interfering with each other.
6
+ * Optionally, it also takes `lockId` param, in which case only actions with
7
+ * the same lock id are blocking each other.
8
+ *
9
+ * Example:
10
+ *
11
+ * ```
12
+ * const synchronize = getSynchronize();
13
+ * synchronize(() => asyncAction1());
14
+ * synchronize(() => asyncAction2());
15
+ * synchronize(() => asyncAction3(), 'differentLockId');
16
+ * ```
17
+ */
18
+ export const getSynchronize = (mutex?: ReturnType<typeof getMutex>) => {
19
+ const lock = mutex ?? getMutex();
20
+
21
+ return <T>(action: () => T, lockId?: keyof any): T extends Promise<unknown> ? T : Promise<T> =>
22
+ lock(lockId).then(unlock => Promise.resolve().then(action).finally(unlock)) as any;
23
+ };
24
+
25
+ export type Synchronize = ReturnType<typeof getSynchronize>;