@onekeyfe/hd-core 1.2.2-alpha.9 → 1.2.2

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 (81) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
  3. package/__tests__/DeviceCommands.test.ts +254 -1
  4. package/__tests__/core-error-output.test.ts +169 -1
  5. package/__tests__/device-lifecycle-events.test.ts +395 -15
  6. package/__tests__/logBlockEvent.test.ts +45 -122
  7. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  8. package/__tests__/open-wallet-session.test.ts +8 -358
  9. package/__tests__/protocol-v2.test.ts +130 -0
  10. package/__tests__/public-device-state-api.test.ts +2 -7
  11. package/__tests__/search-devices.test.ts +196 -8
  12. package/__tests__/sol-sign-offchain-message.test.ts +64 -0
  13. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  14. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  15. package/dist/api/GetFeatures.d.ts.map +1 -1
  16. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  17. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  18. package/dist/api/SearchDevices.d.ts +2 -15
  19. package/dist/api/SearchDevices.d.ts.map +1 -1
  20. package/dist/api/UploadPortfolio.d.ts +1 -0
  21. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  22. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
  23. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  24. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
  25. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  26. package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
  27. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  28. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  29. package/dist/core/RequestQueue.d.ts +1 -0
  30. package/dist/core/RequestQueue.d.ts.map +1 -1
  31. package/dist/core/index.d.ts +3 -1
  32. package/dist/core/index.d.ts.map +1 -1
  33. package/dist/core/uiPromiseRegistry.d.ts +1 -1
  34. package/dist/data-manager/TransportManager.d.ts +2 -0
  35. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  36. package/dist/device/Device.d.ts +2 -0
  37. package/dist/device/Device.d.ts.map +1 -1
  38. package/dist/device/DeviceCommands.d.ts +5 -4
  39. package/dist/device/DeviceCommands.d.ts.map +1 -1
  40. package/dist/events/logBlockEvent.d.ts.map +1 -1
  41. package/dist/index.d.ts +35 -30
  42. package/dist/index.js +587 -314
  43. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  44. package/dist/types/api/getFeatures.d.ts.map +1 -1
  45. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  46. package/dist/types/api/openWalletSession.d.ts +1 -10
  47. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  48. package/dist/types/api/protocolV2.d.ts +3 -4
  49. package/dist/types/api/protocolV2.d.ts.map +1 -1
  50. package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
  51. package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
  52. package/dist/types/params.d.ts.map +1 -1
  53. package/dist/utils/patch.d.ts +1 -1
  54. package/dist/utils/patch.d.ts.map +1 -1
  55. package/package.json +4 -4
  56. package/src/api/FirmwareUpdateV4.ts +9 -4
  57. package/src/api/GetFeatures.ts +1 -0
  58. package/src/api/GetPassphraseState.ts +1 -0
  59. package/src/api/OpenWalletSession.ts +7 -77
  60. package/src/api/SearchDevices.ts +121 -27
  61. package/src/api/UploadPortfolio.ts +5 -4
  62. package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
  63. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
  64. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  65. package/src/api/device/DeviceVerify.ts +8 -0
  66. package/src/api/solana/SolSignOffchainMessage.ts +44 -4
  67. package/src/core/RequestQueue.ts +20 -0
  68. package/src/core/index.ts +159 -52
  69. package/src/data/messages/messages-protocol-v2.json +49 -33
  70. package/src/data/messages/messages.json +8 -5
  71. package/src/data-manager/TransportManager.ts +14 -3
  72. package/src/device/Device.ts +58 -27
  73. package/src/device/DeviceCommands.ts +29 -2
  74. package/src/events/logBlockEvent.ts +6 -75
  75. package/src/protocols/protocol-v2/walletSession.ts +14 -2
  76. package/src/types/api/getFeatures.ts +2 -1
  77. package/src/types/api/getPassphraseState.ts +2 -5
  78. package/src/types/api/openWalletSession.ts +7 -19
  79. package/src/types/api/protocolV2.ts +3 -4
  80. package/src/types/api/solSignOffchainMessage.ts +2 -0
  81. package/src/types/params.ts +7 -0
@@ -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,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;