@onekeyfe/hd-core 1.2.2-alpha.12 → 1.2.2-alpha.120

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 (67) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +452 -9
  3. package/__tests__/DeviceCommands.test.ts +254 -1
  4. package/__tests__/core-error-output.test.ts +92 -1
  5. package/__tests__/device-lifecycle-events.test.ts +379 -15
  6. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  7. package/__tests__/open-wallet-session.test.ts +8 -411
  8. package/__tests__/protocol-v2.test.ts +86 -0
  9. package/__tests__/public-device-state-api.test.ts +2 -7
  10. package/__tests__/sol-sign-offchain-message.test.ts +64 -0
  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/UploadPortfolio.d.ts +1 -0
  15. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  16. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  17. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +6 -0
  18. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
  20. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  21. package/dist/api/solana/SolSignOffchainMessage.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 +3 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/uiPromiseRegistry.d.ts +1 -1
  27. package/dist/device/Device.d.ts +2 -0
  28. package/dist/device/Device.d.ts.map +1 -1
  29. package/dist/device/DeviceCommands.d.ts +5 -4
  30. package/dist/device/DeviceCommands.d.ts.map +1 -1
  31. package/dist/index.d.ts +35 -30
  32. package/dist/index.js +402 -208
  33. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  34. package/dist/types/api/getFeatures.d.ts.map +1 -1
  35. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  36. package/dist/types/api/openWalletSession.d.ts +1 -10
  37. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  38. package/dist/types/api/protocolV2.d.ts +3 -4
  39. package/dist/types/api/protocolV2.d.ts.map +1 -1
  40. package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
  41. package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
  42. package/dist/types/params.d.ts.map +1 -1
  43. package/dist/utils/patch.d.ts +1 -1
  44. package/dist/utils/patch.d.ts.map +1 -1
  45. package/package.json +4 -4
  46. package/src/api/GetFeatures.ts +1 -0
  47. package/src/api/GetPassphraseState.ts +1 -0
  48. package/src/api/OpenWalletSession.ts +7 -77
  49. package/src/api/UploadPortfolio.ts +5 -4
  50. package/src/api/allnetwork/AllNetworkGetAddress.ts +47 -20
  51. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +88 -22
  52. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  53. package/src/api/device/DeviceVerify.ts +8 -0
  54. package/src/api/solana/SolSignOffchainMessage.ts +44 -4
  55. package/src/core/RequestQueue.ts +20 -0
  56. package/src/core/index.ts +116 -49
  57. package/src/data/messages/messages-protocol-v2.json +49 -33
  58. package/src/data/messages/messages.json +8 -5
  59. package/src/device/Device.ts +51 -27
  60. package/src/device/DeviceCommands.ts +29 -2
  61. package/src/protocols/protocol-v2/walletSession.ts +7 -0
  62. package/src/types/api/getFeatures.ts +2 -1
  63. package/src/types/api/getPassphraseState.ts +2 -5
  64. package/src/types/api/openWalletSession.ts +7 -19
  65. package/src/types/api/protocolV2.ts +3 -4
  66. package/src/types/api/solSignOffchainMessage.ts +2 -0
  67. package/src/types/params.ts +7 -0
@@ -1,7 +1,6 @@
1
1
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
3
3
 
4
- import { deviceWalletSessionStore } from '../device/DeviceWalletSessionStore';
5
4
  import { getProtocolV2WalletSession } from '../protocols/protocol-v2/walletSession';
6
5
  import { getPassphraseStateWithRefreshDeviceInfo } from '../utils/deviceFeaturesUtils';
7
6
  import { BaseMethod } from './BaseMethod';
@@ -13,16 +12,6 @@ import type {
13
12
  OpenWalletSessionPayload,
14
13
  } from '../types/api/openWalletSession';
15
14
 
16
- const requiredString = (value: unknown, name: string) => {
17
- if (value === undefined || value === null) {
18
- throw invalidParameter(`Missing required parameter: ${name}`);
19
- }
20
- if (typeof value !== 'string' || !value.trim()) {
21
- throw invalidParameter(`Parameter [${name}] must be a non-empty string.`);
22
- }
23
- return value.trim();
24
- };
25
-
26
15
  const wasResumed = (session: unknown) =>
27
16
  !!session &&
28
17
  typeof session === 'object' &&
@@ -53,34 +42,21 @@ const normalizeParams = (payload: Record<string, unknown>): OpenWalletSessionPar
53
42
  }
54
43
  if (
55
44
  payload.mode !== OpenWalletSessionMode.Standard &&
56
- payload.mode !== OpenWalletSessionMode.SelectHidden &&
57
- payload.mode !== OpenWalletSessionMode.ResumeHidden
45
+ payload.mode !== OpenWalletSessionMode.SelectHidden
58
46
  ) {
59
- throw invalidParameter(
60
- 'Parameter [mode] must be one of standard, select-hidden, or resume-hidden.'
61
- );
47
+ throw invalidParameter('Parameter [mode] must be one of standard or select-hidden.');
62
48
  }
63
49
  if (payload.useEmptyPassphrase !== undefined || payload.initSession !== undefined) {
64
50
  throw invalidParameter(
65
51
  'Legacy parameters [useEmptyPassphrase] and [initSession] are not supported by openWalletSession.'
66
52
  );
67
53
  }
68
- if (
69
- payload.mode === OpenWalletSessionMode.Standard ||
70
- payload.mode === OpenWalletSessionMode.SelectHidden
71
- ) {
72
- if (payload.deviceId !== undefined || payload.passphraseState !== undefined) {
73
- throw invalidParameter(
74
- 'Parameters [deviceId] and [passphraseState] are only allowed with mode [resume-hidden].'
75
- );
76
- }
77
- return { mode: payload.mode };
54
+ if (payload.deviceId !== undefined || payload.passphraseState !== undefined) {
55
+ throw invalidParameter(
56
+ 'Parameters [deviceId] and [passphraseState] are not supported by openWalletSession. Pass passphraseState on later address and signing calls.'
57
+ );
78
58
  }
79
- return {
80
- mode: OpenWalletSessionMode.ResumeHidden,
81
- deviceId: requiredString(payload.deviceId, 'deviceId'),
82
- passphraseState: requiredString(payload.passphraseState, 'passphraseState'),
83
- };
59
+ return { mode: payload.mode };
84
60
  };
85
61
 
86
62
  export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParams> {
@@ -196,52 +172,6 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
196
172
  };
197
173
  }
198
174
 
199
- if (this.params.mode === OpenWalletSessionMode.ResumeHidden) {
200
- if (isProtocolV2) {
201
- await ensureProtocolV2WalletStatus();
202
- const refreshedDeviceId = requireDeviceId();
203
- if (refreshedDeviceId !== this.params.deviceId) {
204
- deviceWalletSessionStore.delete(this.params.deviceId, this.params.passphraseState);
205
- throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
206
- }
207
- } else if (requireDeviceId() !== this.params.deviceId) {
208
- throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
209
- }
210
- this.device.passphraseState = this.params.passphraseState;
211
- const cachedSessionId = deviceWalletSessionStore.get(
212
- this.params.deviceId,
213
- this.params.passphraseState
214
- );
215
- if (!cachedSessionId && !isProtocolV2) {
216
- throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
217
- }
218
- if (!isProtocolV2) {
219
- await this.device.initialize({
220
- deviceId: this.params.deviceId,
221
- passphraseState: this.params.passphraseState,
222
- });
223
- }
224
- const session = isProtocolV2
225
- ? await getProtocolV2WalletSession(this.device, {
226
- expectedPassphraseState: this.params.passphraseState,
227
- })
228
- : await getPassphraseStateWithRefreshDeviceInfo(this.device, {
229
- expectPassphraseState: this.params.passphraseState,
230
- });
231
- const deviceId = requireDeviceId();
232
- if (session.passphraseState !== this.params.passphraseState) {
233
- this.device.clearInternalState();
234
- throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
235
- }
236
- return {
237
- protocol,
238
- walletType: 'hidden',
239
- deviceId,
240
- ...requireHiddenWalletResponse(session),
241
- resumed: wasResumed(session) || (!isProtocolV2 && session.newSession === cachedSessionId),
242
- };
243
- }
244
-
245
175
  this.device.passphraseState = undefined;
246
176
  const walletStatus = await ensureProtocolV2WalletStatus();
247
177
  if (isProtocolV2 && walletStatus.status.passphraseProtection !== true) {
@@ -7,6 +7,8 @@ import FileWrite from './FileWrite';
7
7
  export type UploadPortfolioParams = {
8
8
  packageBase64: string;
9
9
  timeoutMs?: number | string;
10
+ /** Controls transfer progress UI events. Defaults to `silent`. */
11
+ uiMode?: 'silent' | 'progress';
10
12
  };
11
13
 
12
14
  const PORTFOLIO_PENDING_PATH = 'vol1:/portfolio/portfolio.okpkg.pending';
@@ -17,7 +19,7 @@ const PORTFOLIO_UPDATE_MESSAGE_TYPE = 61400;
17
19
 
18
20
  export default class UploadPortfolio extends FileWrite {
19
21
  init() {
20
- const { packageBase64, timeoutMs } = this.payload as UploadPortfolioParams;
22
+ const { packageBase64, timeoutMs, uiMode = 'silent' } = this.payload as UploadPortfolioParams;
21
23
  const packageBytes = decodeCanonicalBase64({
22
24
  value: packageBase64,
23
25
  parameterName: 'packageBase64',
@@ -31,13 +33,12 @@ export default class UploadPortfolio extends FileWrite {
31
33
  chunkSize: PORTFOLIO_CHUNK_SIZE,
32
34
  overwrite: true,
33
35
  append: false,
34
- emitProgress: false,
36
+ emitProgress: uiMode === 'progress',
35
37
  timeoutMs,
36
38
  };
37
39
  super.init();
38
40
  this.unlockPolicy = 'none';
39
- // Portfolio is a background write/apply flow and never synthesizes UI events.
40
- this.protocolV2UiMode = 'none';
41
+ this.protocolV2UiMode = uiMode === 'progress' ? 'auto' : 'none';
41
42
  }
42
43
 
43
44
  async run() {
@@ -31,22 +31,20 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
31
31
  originalIndex: index,
32
32
  })
33
33
  );
34
- const groupedMethodParams = methodParams.reduce((groups, param) => {
34
+ // Protocol V2 DeviceSessionGet is the Initialize(session_id) equivalent: the
35
+ // SE wallet stays selected until the next Ask/Get or lock. Nested chain
36
+ // methods still resume once in callMethod; same-method addresses can share
37
+ // that session the way Protocol V1 bundles do.
38
+ const methodGroups = methodParams.reduce((groups, param) => {
35
39
  const group = groups.get(param.methodName) ?? [];
36
40
  group.push(param);
37
41
  groups.set(param.methodName, group);
38
42
  return groups;
39
43
  }, 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
44
 
47
- let i = 0;
48
- for (const [methodName, params] of methodGroups) {
49
- const methodParams = {
45
+ let processed = 0;
46
+ for (const [methodName, params] of methodGroups.entries()) {
47
+ const methodCallParams = {
50
48
  bundle: params.map(param => ({
51
49
  ...param.params,
52
50
  })),
@@ -55,27 +53,56 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
55
53
  if (this.abortController?.signal.aborted) {
56
54
  throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
57
55
  }
58
- // call method
59
- const response = await this.callMethod(methodName, methodParams, rootFingerprint);
56
+ const isProtocolV2 = this.device.isProtocolV2();
57
+ // Displayed addresses must not be replayed if a later item fails.
58
+ const runIndividually =
59
+ isProtocolV2 &&
60
+ params.length > 1 &&
61
+ params.some(param => param._originRequestParams.showOnOneKey !== false);
62
+ let response: AllNetworkAddress[] = [];
63
+ if (!runIndividually) {
64
+ response = await this.callMethod(methodName, methodCallParams, rootFingerprint);
65
+ }
66
+
67
+ // callMethod returns failures only for skippable errors; link, cancellation,
68
+ // and wallet errors throw. Retry silent reads separately to isolate a bad
69
+ // path or unsupported coin while reusing the already selected wallet.
70
+ if (
71
+ isProtocolV2 &&
72
+ params.length > 1 &&
73
+ (runIndividually || response.every(item => !item.success))
74
+ ) {
75
+ response = [];
76
+ for (const param of params) {
77
+ if (this.abortController?.signal.aborted) {
78
+ throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
79
+ }
80
+ const itemResponse = await this.callMethod(
81
+ methodName,
82
+ { bundle: [{ ...param.params }] },
83
+ rootFingerprint
84
+ );
85
+ response.push(...itemResponse);
86
+ }
87
+ }
60
88
 
61
89
  if (this.abortController?.signal.aborted) {
62
90
  throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
63
91
  }
64
92
 
65
- for (let i = 0; i < params.length; i++) {
66
- const { _originRequestParams, _originalIndex } = params[i];
67
- const responseKey = `${_originalIndex}`;
68
- resultMap[responseKey] = {
93
+ for (let index = 0; index < params.length; index++) {
94
+ const { _originRequestParams, _originalIndex } = params[index];
95
+ resultMap[`${_originalIndex}`] = {
69
96
  ..._originRequestParams,
70
- ...response[i],
97
+ ...response[index],
71
98
  };
72
99
  }
73
100
 
74
- if (this.payload?.bundle?.length > 1) {
75
- const progress = Math.round(((i + 1) / this.payload.bundle.length) * 100);
101
+ processed += params.length;
102
+ if (bundle.length > 1) {
103
+ const progress = Math.round((processed / bundle.length) * 100);
76
104
  this.postMessage(createUiMessage(UI_REQUEST.DEVICE_PROGRESS, { progress }));
77
105
  }
78
- i++;
79
106
  }
80
107
 
81
108
  for (let i = 0; i < bundle.length; i++) {
@@ -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,6 +326,23 @@ 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 & {
@@ -388,16 +418,17 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
388
418
  }
389
419
  }
390
420
 
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.
421
+ // Nested chain methods skip callAPI's session gate. Resume the requested
422
+ // wallet once per seed domain; DeviceSessionGet is sticky like V1
423
+ // Initialize, so later addresses and chains reuse it.
395
424
  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
425
  const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
399
426
  const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
400
- if (this.device.isProtocolV2() && shouldResumeWalletSession) {
427
+ if (
428
+ this.device.isProtocolV2() &&
429
+ shouldResumeWalletSession &&
430
+ !this.hasProtocolV2WalletResume(deriveCardano)
431
+ ) {
401
432
  const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
402
433
  this.payload.passphraseState,
403
434
  useEmptyPassphrase,
@@ -408,6 +439,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
408
439
  if (!passphraseStateSafety) {
409
440
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
410
441
  }
442
+ this.markProtocolV2WalletResumed(deriveCardano);
411
443
  }
412
444
  },
413
445
  });
@@ -457,30 +489,64 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
457
489
 
458
490
  abstract getAllNetworkAddress(rootFingerprint: number): Promise<AllNetworkAddress[]>;
459
491
 
492
+ protected async stopAllNetworkLoading(canSend = true) {
493
+ const commands = this.loadingCommands;
494
+ this.loadingCommands = undefined;
495
+ // Never send cleanup through a replacement or disposed connection.
496
+ if (!canSend || !commands || commands.disposed || commands !== this.device.commands) return;
497
+ try {
498
+ await commands.typedCall('DeviceAnimationControl', 'Success', {
499
+ action: PROTO.DeviceAnimationAction.AnimationAction_Stop,
500
+ });
501
+ } catch {
502
+ // Cleanup must not mask the operation result. Firmware also has an idle timeout.
503
+ }
504
+ }
505
+
460
506
  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
- });
507
+ this.loadingCleanupInBackground = false;
508
+ try {
509
+ if (this.device.isProtocolV2() && this.device.getCurrentDeviceType() === EDeviceType.Pro2) {
510
+ const protocolInfo = await this.device.ensureProtocolV2RuntimeContext();
511
+ if (supportsProtocolV2Message(protocolInfo, 60461)) {
512
+ const { commands } = this.device;
513
+ await commands.typedCall('DeviceAnimationControl', 'Success', {
514
+ action: PROTO.DeviceAnimationAction.AnimationAction_Start,
515
+ });
516
+ this.loadingCommands = commands;
517
+ }
518
+ }
467
519
 
468
- if (!this.device.isProtocolV2()) {
469
- this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
470
- }
520
+ const res = await this.device.commands.typedCall('GetPublicKey', 'PublicKey', {
521
+ address_n: [toHardened(44), toHardened(1), toHardened(0)],
522
+ coin_name: 'Testnet',
523
+ script_type: 'SPENDADDRESS',
524
+ show_display: false,
525
+ });
471
526
 
472
- if (res.message.root_fingerprint == null) {
473
- throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter);
474
- }
527
+ if (!this.device.isProtocolV2()) {
528
+ this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
529
+ }
530
+
531
+ if (res.message.root_fingerprint == null) {
532
+ throw ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter);
533
+ }
475
534
 
476
- this.abortController = new AbortController();
535
+ this.abortController = new AbortController();
477
536
 
478
- return this.getAllNetworkAddress(res.message.root_fingerprint).catch(e => {
537
+ return await this.getAllNetworkAddress(res.message.root_fingerprint);
538
+ } catch (e) {
539
+ // A failed call may have invalidated the transport link without disposing
540
+ // DeviceCommands. Let firmware time out instead of reconnecting for Stop.
541
+ await this.stopAllNetworkLoading(false);
479
542
  if (e instanceof HardwareError && e.errorCode === HardwareErrorCode.RepeatUnlocking) {
480
543
  throw ERRORS.TypedError(HardwareErrorCode.RepeatUnlocking, e.message);
481
544
  }
482
545
  throw e;
483
- });
546
+ } finally {
547
+ // The callback API returns before its chain requests finish.
548
+ if (!this.loadingCleanupInBackground) await this.stopAllNetworkLoading();
549
+ }
484
550
  }
485
551
  }
486
552
 
@@ -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,11 +1,36 @@
1
1
  import { UI_REQUEST } from '../../constants/ui-request';
2
2
  import { validatePath } from '../helpers/pathUtils';
3
3
  import { BaseMethod } from '../BaseMethod';
4
- import { validateParams } from '../helpers/paramsValidator';
5
- import { stripHexPrefix } from '../helpers/hexUtils';
4
+ import { invalidParameter, validateParams } from '../helpers/paramsValidator';
5
+ import { addHexPrefix, isHexString, stripHexPrefix } from '../helpers/hexUtils';
6
6
 
7
7
  import type { SolanaSignOffChainMessage as HardwareSolSignOffChainMessage } from '@onekeyfe/hd-transport';
8
8
 
9
+ const SOLANA_PUBLIC_KEY_LENGTH = 32;
10
+ const SOLANA_APPLICATION_DOMAIN_LENGTH = 32;
11
+
12
+ const normalizeRequiredSigners = (requiredSigners: unknown[] = []): string[] => {
13
+ const normalized = requiredSigners.map((signer, index) => {
14
+ if (
15
+ typeof signer !== 'string' ||
16
+ !isHexString(addHexPrefix(signer), SOLANA_PUBLIC_KEY_LENGTH)
17
+ ) {
18
+ throw invalidParameter(
19
+ `Parameter [requiredSigners][${index}] must be a ${SOLANA_PUBLIC_KEY_LENGTH}-byte hex public key.`
20
+ );
21
+ }
22
+ return stripHexPrefix(signer).toLowerCase();
23
+ });
24
+
25
+ for (let index = 1; index < normalized.length; index += 1) {
26
+ if (normalized[index - 1] >= normalized[index]) {
27
+ throw invalidParameter('Parameter [requiredSigners] must be strictly sorted and unique.');
28
+ }
29
+ }
30
+
31
+ return normalized;
32
+ };
33
+
9
34
  export default class SolSignOffchainMessage extends BaseMethod<HardwareSolSignOffChainMessage> {
10
35
  getSupportedProtocols() {
11
36
  return ['V1', 'V2'] as const;
@@ -23,10 +48,24 @@ export default class SolSignOffchainMessage extends BaseMethod<HardwareSolSignOf
23
48
  { name: 'messageVersion', type: 'number', required: false },
24
49
  { name: 'messageFormat', type: 'number', required: false },
25
50
  { name: 'applicationDomainHex', type: 'hexString', required: false },
51
+ { name: 'requiredSigners', type: 'array', required: false, allowEmpty: true },
26
52
  ]);
27
53
 
28
- const { path, messageHex, messageVersion, messageFormat, applicationDomainHex } = this.payload;
54
+ const {
55
+ path,
56
+ messageHex,
57
+ messageVersion,
58
+ messageFormat,
59
+ applicationDomainHex,
60
+ requiredSigners,
61
+ } = this.payload;
29
62
  const addressN = validatePath(path, 3);
63
+ if (
64
+ applicationDomainHex !== undefined &&
65
+ !isHexString(addHexPrefix(applicationDomainHex), SOLANA_APPLICATION_DOMAIN_LENGTH)
66
+ ) {
67
+ throw invalidParameter('Parameter [applicationDomainHex] must be 32 bytes.');
68
+ }
30
69
 
31
70
  // init params
32
71
  this.params = {
@@ -34,7 +73,8 @@ export default class SolSignOffchainMessage extends BaseMethod<HardwareSolSignOf
34
73
  message: stripHexPrefix(messageHex),
35
74
  message_version: messageVersion ?? undefined,
36
75
  message_format: messageFormat ?? undefined,
37
- application_domain: applicationDomainHex ?? undefined,
76
+ application_domain: applicationDomainHex ? stripHexPrefix(applicationDomainHex) : undefined,
77
+ required_signers: normalizeRequiredSigners(requiredSigners),
38
78
  };
39
79
  }
40
80
 
@@ -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;