@onekeyfe/hd-core 1.2.3-alpha.1 → 1.2.3-alpha.11

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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
  3. package/__tests__/core-error-output.test.ts +169 -1
  4. package/__tests__/device-lifecycle-events.test.ts +350 -2
  5. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  6. package/__tests__/open-wallet-session.test.ts +8 -411
  7. package/__tests__/protocol-v2.test.ts +51 -0
  8. package/__tests__/public-device-state-api.test.ts +2 -7
  9. package/__tests__/search-devices.test.ts +196 -8
  10. package/__tests__/sol-sign-offchain-message.test.ts +0 -8
  11. package/dist/api/GetFeatures.d.ts.map +1 -1
  12. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  13. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  14. package/dist/api/SearchDevices.d.ts +2 -15
  15. package/dist/api/SearchDevices.d.ts.map +1 -1
  16. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
  17. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  18. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  20. package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
  21. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  22. package/dist/core/RequestQueue.d.ts +1 -0
  23. package/dist/core/RequestQueue.d.ts.map +1 -1
  24. package/dist/core/index.d.ts +1 -0
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/data-manager/TransportManager.d.ts +2 -0
  27. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  28. package/dist/device/Device.d.ts +2 -0
  29. package/dist/device/Device.d.ts.map +1 -1
  30. package/dist/index.d.ts +17 -19
  31. package/dist/index.js +449 -184
  32. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  33. package/dist/types/api/getFeatures.d.ts.map +1 -1
  34. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  35. package/dist/types/api/openWalletSession.d.ts +1 -10
  36. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  37. package/dist/types/params.d.ts.map +1 -1
  38. package/dist/utils/patch.d.ts +1 -1
  39. package/dist/utils/patch.d.ts.map +1 -1
  40. package/package.json +4 -4
  41. package/src/api/GetFeatures.ts +1 -0
  42. package/src/api/GetPassphraseState.ts +1 -0
  43. package/src/api/OpenWalletSession.ts +7 -77
  44. package/src/api/SearchDevices.ts +121 -27
  45. package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
  46. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
  47. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  48. package/src/api/device/DeviceVerify.ts +8 -0
  49. package/src/core/RequestQueue.ts +20 -0
  50. package/src/core/index.ts +135 -43
  51. package/src/data/messages/messages-protocol-v2.json +21 -0
  52. package/src/data-manager/TransportManager.ts +14 -3
  53. package/src/device/Device.ts +58 -27
  54. package/src/protocols/protocol-v2/walletSession.ts +7 -0
  55. package/src/types/api/getFeatures.ts +2 -1
  56. package/src/types/api/getPassphraseState.ts +2 -5
  57. package/src/types/api/openWalletSession.ts +7 -19
  58. package/src/types/params.ts +7 -0
@@ -1,4 +1,4 @@
1
- import { canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { BaseMethod } from './BaseMethod';
4
4
  import TransportManager from '../data-manager/TransportManager';
@@ -6,10 +6,80 @@ import { DataManager } from '../data-manager';
6
6
  import { LoggerNames, getDeviceTypeByBleName, getLogger } from '../utils';
7
7
  import { DevicePool } from '../device/DevicePool';
8
8
 
9
+ import type { SearchDevice } from '../types/device';
9
10
  import type DeviceConnector from '../device/DeviceConnector';
11
+ import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
12
+ import type { Device } from '../device/Device';
10
13
 
11
14
  const Log = getLogger(LoggerNames.DevicePool);
12
15
 
16
+ type RequestQueueLookup = {
17
+ getRequestTasksIdByConnectId: (connectId: string) => number[];
18
+ getTask: (requestId: number) => { method: { device?: Device } } | undefined;
19
+ };
20
+
21
+ const getDescriptorKeys = (descriptor: DeviceDescriptor) =>
22
+ [descriptor.path, descriptor.id].filter(
23
+ (key): key is string => typeof key === 'string' && key.length > 0
24
+ );
25
+
26
+ const isOwnedByActiveWebUsbRequest = (
27
+ descriptor: DeviceDescriptor,
28
+ requestQueue?: RequestQueueLookup
29
+ ) => {
30
+ if (!requestQueue) return false;
31
+ return getDescriptorKeys(descriptor).some(
32
+ key => requestQueue.getRequestTasksIdByConnectId(key).length > 0
33
+ );
34
+ };
35
+
36
+ const getUsbHandle = (descriptor?: DeviceDescriptor) =>
37
+ (descriptor as { device?: unknown } | undefined)?.device;
38
+
39
+ const getOwningRequestDevice = (
40
+ descriptor: DeviceDescriptor,
41
+ requestQueue?: RequestQueueLookup
42
+ ) => {
43
+ if (!requestQueue) return undefined;
44
+ const usbHandle = getUsbHandle(descriptor);
45
+ // A path alone can be shared (the all-zero bootloader path, a synthesized serial-less
46
+ // path), so the owner's identity is only reported for the USB handle it actually bound.
47
+ if (usbHandle === undefined) return undefined;
48
+ for (const key of getDescriptorKeys(descriptor)) {
49
+ for (const requestId of requestQueue.getRequestTasksIdByConnectId(key)) {
50
+ const device = requestQueue.getTask(requestId)?.method.device;
51
+ if (
52
+ device?.features &&
53
+ (device.mainId === key || device.originalDescriptor?.path === key) &&
54
+ getUsbHandle(device.originalDescriptor) === usbHandle
55
+ ) {
56
+ return device;
57
+ }
58
+ }
59
+ }
60
+ return undefined;
61
+ };
62
+
63
+ // A USB path is a routing key (the USB serial, a synthesized usb-vid-pid-name, or the
64
+ // bootloader placeholder), not a hardware identity, so it never fills uuid/serialNo.
65
+ const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
66
+ const connectId = descriptor.path || descriptor.id || null;
67
+ return {
68
+ connectId,
69
+ uuid: '',
70
+ serialNo: null,
71
+ deviceId: null,
72
+ deviceType: EDeviceType.Unknown,
73
+ name: descriptor.name || connectId || '',
74
+ commType: descriptor.commType,
75
+ };
76
+ };
77
+
78
+ const toSearchDevice = (device: Device): SearchDevice | null => {
79
+ const message = device.toMessageObject();
80
+ return message ? (message as SearchDevice) : null;
81
+ };
82
+
13
83
  export default class SearchDevices extends BaseMethod {
14
84
  connector?: DeviceConnector;
15
85
 
@@ -19,19 +89,31 @@ export default class SearchDevices extends BaseMethod {
19
89
  this.skipForceUpdateCheck = true;
20
90
  }
21
91
 
22
- async run() {
23
- await TransportManager.configure();
92
+ async run(): Promise<SearchDevice[]> {
93
+ const env = DataManager.getSettings('env');
94
+ const isWebUsb = env === 'webusb' || env === 'desktop-webusb';
95
+ const requestQueue = this.context?.requestQueue;
96
+ const hasActiveWebUsbRequest = isWebUsb && (requestQueue?.getRequestTasksId().length ?? 0) > 0;
97
+ // Bring up WebUSB even when schema configuration is deferred while a
98
+ // business request owns the discovery lock. Failures here used to be
99
+ // swallowed by configure(); keep searchDevices resolving empty.
100
+ if (isWebUsb) {
101
+ try {
102
+ await TransportManager.ensureInitialized();
103
+ } catch (error) {
104
+ Log.debug('WebUSB bring-up unavailable', error);
105
+ }
106
+ }
107
+ if (!hasActiveWebUsbRequest) await TransportManager.configure();
24
108
  const deviceDiff = await this.connector?.enumerate();
25
109
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
26
110
 
27
- const env = DataManager.getSettings('env');
28
-
29
111
  /**
30
112
  * No need to call features during Bluetooth scaning
31
113
  * to avoid device pairing
32
114
  */
33
115
  if (DataManager.isBleConnect(env)) {
34
- const devices = [];
116
+ const devices: SearchDevice[] = [];
35
117
  const seenIds = new Set<string>();
36
118
 
37
119
  for (const device of devicesDescriptor) {
@@ -56,30 +138,42 @@ export default class SearchDevices extends BaseMethod {
56
138
  return devices;
57
139
  }
58
140
 
59
- const deviceList = [];
141
+ const deviceList: SearchDevice[] = [];
60
142
  for (const descriptor of devicesDescriptor) {
61
- try {
62
- // Discovery is best effort. Browsers may retain WebUSB grants for devices that
63
- // are offline, busy, or not ready, so one descriptor must not abort the scan.
64
- const result = await DevicePool.getDevices([descriptor], descriptor.path, {
65
- // Discovery must actively identify the protocol instead of trusting a caller hint.
66
- connectProtocol: undefined,
67
- forceProtocolDetection: true,
68
- refreshRuntimeState: true,
69
- });
70
- deviceList.push(...result.deviceList);
71
- } catch (error) {
72
- const errorCode =
73
- error && typeof error === 'object' && 'errorCode' in error
74
- ? (error as { errorCode?: unknown }).errorCode
75
- : undefined;
76
- Log.debug('Skip unavailable device during search', {
77
- path: descriptor.path,
78
- ...(errorCode !== undefined ? { errorCode } : {}),
79
- });
143
+ if (hasActiveWebUsbRequest && isOwnedByActiveWebUsbRequest(descriptor, requestQueue)) {
144
+ const cached = DevicePool.getDeviceByPath(descriptor.path);
145
+ const known = cached?.features ? cached : getOwningRequestDevice(descriptor, requestQueue);
146
+ const message = known ? toSearchDevice(known) : null;
147
+ // Do not probe a path an in-flight request already owns. A cache miss is still a
148
+ // connected device: report the owning request's device, or the path with no identity.
149
+ deviceList.push(message ?? toSearchDeviceFromDescriptor(descriptor));
150
+ } else {
151
+ try {
152
+ // Discovery is best effort. Browsers may retain WebUSB grants for devices that
153
+ // are offline, busy, or not ready, so one descriptor must not abort the scan.
154
+ const result = await DevicePool.getDevices([descriptor], descriptor.path, {
155
+ // Discovery must actively identify the protocol instead of trusting a caller hint.
156
+ connectProtocol: undefined,
157
+ forceProtocolDetection: true,
158
+ refreshRuntimeState: true,
159
+ });
160
+ for (const device of result.deviceList) {
161
+ const message = toSearchDevice(device);
162
+ if (message) deviceList.push(message);
163
+ }
164
+ } catch (error) {
165
+ const errorCode =
166
+ error && typeof error === 'object' && 'errorCode' in error
167
+ ? (error as { errorCode?: unknown }).errorCode
168
+ : undefined;
169
+ Log.debug('Skip unavailable device during search', {
170
+ path: descriptor.path,
171
+ ...(errorCode !== undefined ? { errorCode } : {}),
172
+ });
173
+ }
80
174
  }
81
175
  }
82
176
 
83
- return deviceList.map(device => device.toMessageObject());
177
+ return deviceList;
84
178
  }
85
179
  }
@@ -7,21 +7,74 @@ import AllNetworkGetAddressBase from './AllNetworkGetAddressBase';
7
7
  import type { CoreApi } from '../../types';
8
8
  import type {
9
9
  AllNetworkAddress,
10
- AllNetworkAddressParams,
11
10
  AllNetworkGetAddressParams,
12
11
  } from '../../types/api/allNetworkGetAddress';
13
12
 
14
- type MethodParams = {
15
- methodName: keyof CoreApi;
16
- params: Parameters<CoreApi[keyof CoreApi]>[0];
17
- _originRequestParams: AllNetworkAddressParams;
18
- _originalIndex: number;
19
- };
13
+ type MethodParams = ReturnType<AllNetworkGetAddressBase['generateMethodName']>;
20
14
 
21
15
  export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
16
+ private checkAborted() {
17
+ if (this.abortController?.signal.aborted) {
18
+ throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
19
+ }
20
+ }
21
+
22
+ private async callAddressGroup(
23
+ methodName: keyof CoreApi,
24
+ params: MethodParams[],
25
+ rootFingerprint: number
26
+ ): Promise<AllNetworkAddress[]> {
27
+ const methodCallParams = { bundle: params.map(param => ({ ...param.params })) };
28
+ if (!this.device.isProtocolV2() || params.length === 1) {
29
+ return this.callMethod(methodName, methodCallParams, rootFingerprint);
30
+ }
31
+
32
+ const postedAddressCounts = new Map<string, number>();
33
+ let runningIndividually = false;
34
+ const postMessage: typeof this.postMessage = message => {
35
+ if (message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT) {
36
+ const { path, address } = message.payload.data;
37
+ const key = JSON.stringify([path, address]);
38
+ const count = postedAddressCounts.get(key) ?? 0;
39
+ if (runningIndividually && count > 0) {
40
+ postedAddressCounts.set(key, count - 1);
41
+ return;
42
+ }
43
+ if (!runningIndividually) postedAddressCounts.set(key, count + 1);
44
+ }
45
+ this.postMessage(message);
46
+ };
47
+
48
+ // Only silent reads may be replayed. Forward their notifications immediately,
49
+ // then suppress matching retry copies by count so repeated inputs still emit.
50
+ if (params.every(param => param._originRequestParams.showOnOneKey === false)) {
51
+ const response = await this.callMethod(
52
+ methodName,
53
+ methodCallParams,
54
+ rootFingerprint,
55
+ postMessage
56
+ );
57
+ // Skippable errors become failed items; link, cancellation and wallet errors throw.
58
+ if (response.some(item => item.success)) return response;
59
+ }
60
+
61
+ runningIndividually = true;
62
+ const responses: AllNetworkAddress[] = [];
63
+ for (const param of params) {
64
+ this.checkAborted();
65
+ const response = await this.callMethod(
66
+ methodName,
67
+ { bundle: [{ ...param.params }] },
68
+ rootFingerprint,
69
+ postMessage
70
+ );
71
+ responses.push(...response);
72
+ }
73
+ return responses;
74
+ }
75
+
22
76
  async getAllNetworkAddress(rootFingerprint: number) {
23
77
  const responses: AllNetworkAddress[] = [];
24
- const resultMap: Record<string, AllNetworkAddress> = {};
25
78
  const { bundle } = this.payload as AllNetworkGetAddressParams;
26
79
 
27
80
  const methodParams = bundle.map((param, index) =>
@@ -31,58 +84,39 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
31
84
  originalIndex: index,
32
85
  })
33
86
  );
34
- const groupedMethodParams = methodParams.reduce((groups, param) => {
87
+ // Protocol V2 DeviceSessionGet is the Initialize(session_id) equivalent: the
88
+ // SE wallet stays selected until the next Ask/Get or lock. Nested chain
89
+ // methods still resume once in callMethod; same-method addresses can share
90
+ // that session the way Protocol V1 bundles do.
91
+ const methodGroups = methodParams.reduce((groups, param) => {
35
92
  const group = groups.get(param.methodName) ?? [];
36
93
  group.push(param);
37
94
  groups.set(param.methodName, group);
38
95
  return groups;
39
96
  }, new Map<keyof CoreApi, MethodParams[]>());
40
- const requiresProtocolV2WalletHandoff =
41
- this.device.isProtocolV2() &&
42
- (this.payload.useEmptyPassphrase === true || !!this.payload.passphraseState);
43
- const methodGroups: [keyof CoreApi, MethodParams[]][] = requiresProtocolV2WalletHandoff
44
- ? methodParams.map(param => [param.methodName, [param]])
45
- : Array.from(groupedMethodParams.entries());
46
97
 
47
- let i = 0;
48
- for (const [methodName, params] of methodGroups) {
49
- const methodParams = {
50
- bundle: params.map(param => ({
51
- ...param.params,
52
- })),
53
- };
54
-
55
- if (this.abortController?.signal.aborted) {
56
- throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
57
- }
58
- // call method
59
- const response = await this.callMethod(methodName, methodParams, rootFingerprint);
98
+ let processed = 0;
99
+ for (const [methodName, params] of methodGroups.entries()) {
100
+ this.checkAborted();
101
+ const response = await this.callAddressGroup(methodName, params, rootFingerprint);
102
+ this.checkAborted();
60
103
 
61
- if (this.abortController?.signal.aborted) {
62
- throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
63
- }
64
-
65
- for (let i = 0; i < params.length; i++) {
66
- const { _originRequestParams, _originalIndex } = params[i];
67
- const responseKey = `${_originalIndex}`;
68
- resultMap[responseKey] = {
104
+ for (let index = 0; index < params.length; index++) {
105
+ const { _originRequestParams, _originalIndex } = params[index];
106
+ responses[_originalIndex] = {
69
107
  ..._originRequestParams,
70
- ...response[i],
108
+ ...response[index],
71
109
  };
72
110
  }
73
111
 
74
- if (this.payload?.bundle?.length > 1) {
75
- const progress = Math.round(((i + 1) / this.payload.bundle.length) * 100);
112
+ processed += params.length;
113
+ if (bundle.length > 1) {
114
+ const progress = Math.round((processed / bundle.length) * 100);
76
115
  this.postMessage(createUiMessage(UI_REQUEST.DEVICE_PROGRESS, { progress }));
77
116
  }
78
- i++;
79
- }
80
-
81
- for (let i = 0; i < bundle.length; i++) {
82
- responses.push(resultMap[i]);
83
117
  }
84
118
 
85
119
  this.abortController = null;
86
- return Promise.resolve(responses);
120
+ return responses;
87
121
  }
88
122
  }
@@ -1,5 +1,6 @@
1
1
  import semver from 'semver';
2
2
  import {
3
+ EDeviceType,
3
4
  ERRORS,
4
5
  HardwareError,
5
6
  HardwareErrorCode,
@@ -15,6 +16,7 @@ import { DEVICE, IFRAME, createUiMessage } from '../../events';
15
16
  import { UI_REQUEST } from '../../constants/ui-request';
16
17
  import { onDeviceButtonHandler } from '../../core';
17
18
  import { runMethodWithUnlockPolicy } from '../../protocols/protocol-v2/unlockPolicyRunner';
19
+ import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
18
20
  import {
19
21
  completeRequestContext,
20
22
  createRequestContext,
@@ -22,6 +24,7 @@ import {
22
24
  } from '../../utils/tracing';
23
25
 
24
26
  import type { Device, DeviceEvents } from '../../device/Device';
27
+ import type { DeviceCommands } from '../../device/DeviceCommands';
25
28
  import type { CoreApi } from '../../types';
26
29
  import type {
27
30
  AllNetworkAddress,
@@ -266,6 +269,16 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
266
269
 
267
270
  abortController: AbortController | null = null;
268
271
 
272
+ protected loadingCleanupInBackground = false;
273
+
274
+ private loadingCommands?: DeviceCommands;
275
+
276
+ // DeviceSessionGet selects the SE wallet like Initialize(session_id). Nested
277
+ // all-network methods skip callAPI, so the first chain call still resumes;
278
+ // later same-domain calls reuse that session. Cardano may Ask [Standard,
279
+ // Cardano] once, which also covers later non-Cardano commands.
280
+ private protocolV2ResumedSeedDomains = new Set<'standard' | 'cardano'>();
281
+
269
282
  init() {
270
283
  this.checkDeviceId = true;
271
284
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
@@ -313,12 +326,30 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
313
326
  };
314
327
  }
315
328
 
329
+ private hasProtocolV2WalletResume(deriveCardano?: boolean) {
330
+ if (deriveCardano) {
331
+ return this.protocolV2ResumedSeedDomains.has('cardano');
332
+ }
333
+ return (
334
+ this.protocolV2ResumedSeedDomains.has('standard') ||
335
+ this.protocolV2ResumedSeedDomains.has('cardano')
336
+ );
337
+ }
338
+
339
+ private markProtocolV2WalletResumed(deriveCardano?: boolean) {
340
+ this.protocolV2ResumedSeedDomains.add('standard');
341
+ if (deriveCardano) {
342
+ this.protocolV2ResumedSeedDomains.add('cardano');
343
+ }
344
+ }
345
+
316
346
  async callMethod(
317
347
  methodName: keyof CoreApi,
318
348
  params: any & {
319
349
  bundle: (any & { _originRequestParams: CommonResponseParams })[];
320
350
  },
321
- rootFingerprint: number
351
+ rootFingerprint: number,
352
+ postMessage = this.postMessage
322
353
  ) {
323
354
  const method: BaseMethod = findMethod({
324
355
  event: IFRAME.CALL,
@@ -332,7 +363,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
332
363
  });
333
364
 
334
365
  method.connector = this.connector;
335
- method.postMessage = this.postMessage;
366
+ method.postMessage = postMessage;
336
367
  if (this.context) {
337
368
  method.setContext?.(this.context);
338
369
  }
@@ -388,16 +419,17 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
388
419
  }
389
420
  }
390
421
 
391
- // Protocol V2 hands a wallet session to exactly one blockchain request.
392
- // The parent all-network call consumes its first handoff while fetching
393
- // the root fingerprint, so each nested chain method must resume the
394
- // requested standard or hidden wallet before sending its device command.
422
+ // Nested chain methods skip callAPI's session gate. Resume the requested
423
+ // wallet once per seed domain; DeviceSessionGet is sticky like V1
424
+ // Initialize, so later addresses and chains reuse it.
395
425
  const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
396
- // Nested Cardano methods opt in to [Standard, Cardano] if Ask rebuilds.
397
- // Other chains stay Standard-only.
398
426
  const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
399
427
  const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
400
- if (this.device.isProtocolV2() && shouldResumeWalletSession) {
428
+ if (
429
+ this.device.isProtocolV2() &&
430
+ shouldResumeWalletSession &&
431
+ !this.hasProtocolV2WalletResume(deriveCardano)
432
+ ) {
401
433
  const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
402
434
  this.payload.passphraseState,
403
435
  useEmptyPassphrase,
@@ -408,6 +440,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
408
440
  if (!passphraseStateSafety) {
409
441
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
410
442
  }
443
+ this.markProtocolV2WalletResumed(deriveCardano);
411
444
  }
412
445
  },
413
446
  });
@@ -457,30 +490,68 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
457
490
 
458
491
  abstract getAllNetworkAddress(rootFingerprint: number): Promise<AllNetworkAddress[]>;
459
492
 
493
+ protected async stopAllNetworkLoading(canSend = true) {
494
+ const commands = this.loadingCommands;
495
+ this.loadingCommands = undefined;
496
+ // Never send cleanup through a replacement or disposed connection.
497
+ if (!canSend || !commands || commands.disposed || commands !== this.device.commands) return;
498
+ try {
499
+ await commands.typedCall('DeviceAnimationControl', 'Success', {
500
+ action: PROTO.DeviceAnimationAction.AnimationAction_Stop,
501
+ });
502
+ } catch {
503
+ // Cleanup must not mask the operation result. Firmware also has an idle timeout.
504
+ }
505
+ }
506
+
460
507
  async run() {
461
- const res = await this.device.commands.typedCall('GetPublicKey', 'PublicKey', {
462
- address_n: [toHardened(44), toHardened(1), toHardened(0)],
463
- coin_name: 'Testnet',
464
- script_type: 'SPENDADDRESS',
465
- show_display: false,
466
- });
508
+ this.loadingCleanupInBackground = false;
509
+ try {
510
+ if (
511
+ this.device.isProtocolV2() &&
512
+ (this.device.getCurrentDeviceType() === EDeviceType.Pro2 ||
513
+ this.device.getCurrentDeviceType() === EDeviceType.Neo)
514
+ ) {
515
+ const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
516
+ if (supportsProtocolV2Message(protocolInfo, 60461)) {
517
+ const { commands } = this.device;
518
+ await commands.typedCall('DeviceAnimationControl', 'Success', {
519
+ action: PROTO.DeviceAnimationAction.AnimationAction_Start,
520
+ });
521
+ this.loadingCommands = commands;
522
+ }
523
+ }
467
524
 
468
- if (!this.device.isProtocolV2()) {
469
- this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
470
- }
525
+ const res = await this.device.commands.typedCall('GetPublicKey', 'PublicKey', {
526
+ address_n: [toHardened(44), toHardened(1), toHardened(0)],
527
+ coin_name: 'Testnet',
528
+ script_type: 'SPENDADDRESS',
529
+ show_display: false,
530
+ });
471
531
 
472
- if (res.message.root_fingerprint == null) {
473
- throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter);
474
- }
532
+ if (!this.device.isProtocolV2()) {
533
+ this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
534
+ }
535
+
536
+ if (res.message.root_fingerprint == null) {
537
+ throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter);
538
+ }
475
539
 
476
- this.abortController = new AbortController();
540
+ this.abortController = new AbortController();
477
541
 
478
- return this.getAllNetworkAddress(res.message.root_fingerprint).catch(e => {
542
+ return await this.getAllNetworkAddress(res.message.root_fingerprint);
543
+ } catch (e) {
544
+ // A failed call may have invalidated the transport link without disposing
545
+ // DeviceCommands. Let firmware time out instead of reconnecting for Stop.
546
+ await this.stopAllNetworkLoading(false);
479
547
  if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.RepeatUnlocking) {
480
548
  throw ERRORS.TypedError(HardwareErrorCode.RepeatUnlocking, e.message);
481
549
  }
482
550
  throw e;
483
- });
551
+ } finally {
552
+ // The callback API returns before its chain requests finish.
553
+ if (!this.loadingCleanupInBackground) await this.stopAllNetworkLoading();
554
+ }
484
555
  }
485
556
  }
486
557
 
@@ -28,6 +28,7 @@ export default class AllNetworkGetAddressByLoop extends AllNetworkGetAddressBase
28
28
  const bundle = this.payload.bundle || [this.payload];
29
29
 
30
30
  // process callbacks in background
31
+ this.loadingCleanupInBackground = true;
31
32
  const callbackPromise = this.processCallbacksInBackground(
32
33
  bundle,
33
34
  rootFingerprint,
@@ -95,6 +96,7 @@ export default class AllNetworkGetAddressByLoop extends AllNetworkGetAddressBase
95
96
  data: allResults,
96
97
  });
97
98
  } catch (error: any) {
99
+ await this.stopAllNetworkLoading(false);
98
100
  let errorCode = error.errorCode || error.code;
99
101
  let errorMessage = error.message;
100
102
 
@@ -121,6 +123,7 @@ export default class AllNetworkGetAddressByLoop extends AllNetworkGetAddressBase
121
123
  },
122
124
  });
123
125
  } finally {
126
+ await this.stopAllNetworkLoading();
124
127
  this.context?.cancelCallbackTasks(this.payload.connectId);
125
128
  this.abortController = null;
126
129
  }
@@ -22,6 +22,14 @@ export default class DeviceVerify extends BaseMethod<BixinVerifyDeviceRequest> {
22
22
  // the main PIN or an Attach PIN may authorize them.
23
23
  this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
24
24
  this.useDevicePassphraseState = false;
25
+ this.protocolV2UiInteraction = {
26
+ request: 'button',
27
+ source: 'method-lifecycle',
28
+ reason: 'device-management',
29
+ completion: 'operation-completed',
30
+ deviceOnly: true,
31
+ operation: 'deviceVerify',
32
+ };
25
33
 
26
34
  // check payload
27
35
  validateParams(this.payload, [{ name: 'dataHex', type: 'hexString' }]);
@@ -1,3 +1,5 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
1
3
  import { LoggerNames, getLogger } from '../utils';
2
4
 
3
5
  import type { Deferred } from '@onekeyfe/hd-shared';
@@ -40,6 +42,24 @@ export default class RequestQueue {
40
42
  return this.requestQueue.get(requestId);
41
43
  }
42
44
 
45
+ public async waitForTask<T>(task: RequestTask, pending: () => Promise<T>): Promise<T> {
46
+ const signal = task.method.abortSignal;
47
+ const cancellationError = () => ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
48
+ if (signal?.aborted) throw cancellationError();
49
+ let onAbort: (() => void) | undefined;
50
+ try {
51
+ const cancelled = new Promise<never>((_, reject) => {
52
+ onAbort = () => reject(cancellationError());
53
+ signal?.addEventListener('abort', onAbort, { once: true });
54
+ });
55
+ const result = await Promise.race([pending(), cancelled]);
56
+ if (signal?.aborted) throw cancellationError();
57
+ return result;
58
+ } finally {
59
+ if (onAbort) signal?.removeEventListener('abort', onAbort);
60
+ }
61
+ }
62
+
43
63
  // 获取请求的AbortController
44
64
  public getAbortController(requestId: number) {
45
65
  return this.requestQueue.get(requestId)?.abortController;