@onekeyfe/hd-core 1.2.0-alpha.35 → 1.2.0-alpha.37

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 (100) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +78 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/deviceUploadNft.test.ts +187 -0
  4. package/__tests__/get-device-state.test.ts +104 -12
  5. package/__tests__/homescreen.test.ts +41 -1
  6. package/__tests__/logBlockEvent.test.ts +1 -1
  7. package/__tests__/open-wallet-session.test.ts +259 -18
  8. package/__tests__/pro2Nft.test.ts +108 -0
  9. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  10. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  11. package/__tests__/protocol-v2.test.ts +377 -402
  12. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  13. package/dist/api/BaseMethod.d.ts +3 -1
  14. package/dist/api/BaseMethod.d.ts.map +1 -1
  15. package/dist/api/FirmwareUpdateV4.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/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  19. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  20. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  21. package/dist/api/index.d.ts +1 -0
  22. package/dist/api/index.d.ts.map +1 -1
  23. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  24. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  26. package/dist/core/deviceEventRegistration.d.ts +2 -0
  27. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/device/Device.d.ts +22 -4
  30. package/dist/device/Device.d.ts.map +1 -1
  31. package/dist/events/device.d.ts +4 -0
  32. package/dist/events/device.d.ts.map +1 -1
  33. package/dist/events/logBlockEvent.d.ts.map +1 -1
  34. package/dist/events/ui-request.d.ts +27 -2
  35. package/dist/events/ui-request.d.ts.map +1 -1
  36. package/dist/index.d.ts +88 -7
  37. package/dist/index.js +937 -604
  38. package/dist/inject.d.ts.map +1 -1
  39. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  40. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  41. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  42. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  43. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/api/index.d.ts +2 -1
  49. package/dist/types/api/index.d.ts.map +1 -1
  50. package/dist/types/api/protocolV2.d.ts +3 -0
  51. package/dist/types/api/protocolV2.d.ts.map +1 -1
  52. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  53. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  54. package/dist/utils/homescreen.d.ts +4 -0
  55. package/dist/utils/homescreen.d.ts.map +1 -1
  56. package/dist/utils/index.d.ts +1 -1
  57. package/dist/utils/index.d.ts.map +1 -1
  58. package/dist/utils/patch.d.ts +1 -1
  59. package/dist/utils/patch.d.ts.map +1 -1
  60. package/dist/utils/pro2Nft.d.ts +29 -0
  61. package/dist/utils/pro2Nft.d.ts.map +1 -0
  62. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  63. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  64. package/package.json +4 -4
  65. package/src/api/BaseMethod.ts +8 -10
  66. package/src/api/FirmwareUpdateV4.ts +13 -6
  67. package/src/api/GetPassphraseState.ts +7 -1
  68. package/src/api/OpenWalletSession.ts +54 -17
  69. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  70. package/src/api/device/DeviceUnlock.ts +1 -1
  71. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  72. package/src/api/index.ts +1 -0
  73. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  74. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  75. package/src/core/deviceEventRegistration.ts +4 -0
  76. package/src/core/index.ts +111 -74
  77. package/src/data/messages/messages-protocol-v2.json +10 -0
  78. package/src/device/Device.ts +221 -24
  79. package/src/events/device.ts +4 -0
  80. package/src/events/logBlockEvent.ts +1 -0
  81. package/src/events/ui-request.ts +32 -2
  82. package/src/inject.ts +1 -0
  83. package/src/protocols/protocol-v2/features.ts +9 -2
  84. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  85. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  86. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  87. package/src/protocols/protocol-v2/walletSession.ts +93 -18
  88. package/src/types/api/index.ts +2 -0
  89. package/src/types/api/protocolV2.ts +13 -0
  90. package/src/utils/deviceFeaturesUtils.ts +4 -0
  91. package/src/utils/homescreen.ts +32 -6
  92. package/src/utils/index.ts +6 -1
  93. package/src/utils/pro2Nft.ts +91 -0
  94. package/src/utils/pro2Wallpaper.ts +35 -8
  95. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  96. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  97. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  98. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  99. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  100. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
package/src/core/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
- import { TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
3
+ import { DeviceSessionPinType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
4
4
  import {
5
5
  ERRORS,
6
6
  ERROR_CODES_REQUIRE_RELEASE,
@@ -55,7 +55,7 @@ import DeviceConnector from '../device/DeviceConnector';
55
55
  import RequestQueue from './RequestQueue';
56
56
  import { registerHardwareUiEventListeners } from './deviceEventRegistration';
57
57
  import { getSynchronize } from '../utils/getSynchronize';
58
- import { runMethodWithUnlockRetry } from '../protocols/protocol-v2/unlockRetry';
58
+ import { runMethodWithUnlockPolicy } from '../protocols/protocol-v2/unlockPolicyRunner';
59
59
  import {
60
60
  ProtocolV2UiInteractionCoordinator,
61
61
  isProtocolV2UiEnabled,
@@ -406,6 +406,8 @@ const onCallDevice = async (
406
406
 
407
407
  registerHardwareUiEventListeners(device, {
408
408
  pin: onDevicePinHandler,
409
+ pinOnDevice: onEnterPinOnDeviceHandler,
410
+ pinOnDeviceComplete: onPinOnDeviceCompleteHandler,
409
411
  button: onDeviceButtonHandler,
410
412
  passphrase: message.payload.useEmptyPassphrase
411
413
  ? onEmptyPassphraseHandler
@@ -421,6 +423,7 @@ const onCallDevice = async (
421
423
  );
422
424
 
423
425
  const protocolV2UiCoordinator = new ProtocolV2UiInteractionCoordinator(device, postMessage);
426
+ device.beginProtocolV2UiInteraction();
424
427
  device.on(
425
428
  DEVICE.SELECT_DEVICE_FOR_SWITCH_FIRMWARE_WEB_DEVICE,
426
429
  onSelectDeviceForSwitchFirmwareWebDeviceHandler
@@ -547,89 +550,92 @@ const onCallDevice = async (
547
550
  );
548
551
  }
549
552
 
550
- if (method.deviceId && method.checkDeviceId) {
551
- const isSameDeviceID = await checkLiveDeviceId(device, method.deviceId);
552
- if (!isSameDeviceID) {
553
- return Promise.reject(ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError));
554
- }
555
- }
556
-
557
- /**
558
- * check firmware release info
559
- */
560
- method.checkFirmwareRelease();
553
+ try {
554
+ let deviceIdCheckedDuringUnlockPreflight = false;
555
+ const response = await runMethodWithUnlockPolicy<object>(method, device, {
556
+ uiCoordinator: protocolV2UiCoordinator,
557
+ afterStatusBeforeUnlock: () => {
558
+ if (method.deviceId && method.checkDeviceId) {
559
+ if (!device.checkDeviceId(method.deviceId)) {
560
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
561
+ }
562
+ deviceIdCheckedDuringUnlockPreflight = true;
563
+ }
564
+ },
565
+ prepare: async () => {
566
+ if (method.deviceId && method.checkDeviceId && !deviceIdCheckedDuringUnlockPreflight) {
567
+ const isSameDeviceID = await checkLiveDeviceId(device, method.deviceId);
568
+ if (!isSameDeviceID) {
569
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
570
+ }
571
+ }
561
572
 
562
- /**
563
- * check additional supported feature
564
- */
565
- method.checkDeviceSupportFeature();
573
+ /**
574
+ * check firmware release info
575
+ */
576
+ method.checkFirmwareRelease();
566
577
 
567
- // reconfigure messages
568
- if (_deviceList && device.features && !device.isProtocolV2()) {
569
- await TransportManager.reconfigure(device.features);
570
- }
578
+ /**
579
+ * check additional supported feature
580
+ */
581
+ method.checkDeviceSupportFeature();
571
582
 
572
- // Check to see if it is safe to use Passphrase
573
- checkPassphraseEnableState(method, device.features);
583
+ // reconfigure messages
584
+ if (_deviceList && device.features && !device.isProtocolV2()) {
585
+ await TransportManager.reconfigure(device.features);
586
+ }
574
587
 
575
- if (shouldCheckPassphraseState(method, device)) {
576
- // check version
577
- const support = device.supportNewPassphrase();
578
- if (!support.support) {
579
- return Promise.reject(
580
- ERRORS.TypedError(
581
- HardwareErrorCode.DeviceNotSupportPassphrase,
582
- `Device not support passphrase, please update to ${support.require}`,
583
- {
584
- require: support.require,
588
+ // Check to see if it is safe to use Passphrase
589
+ checkPassphraseEnableState(method, device.features);
590
+
591
+ if (shouldCheckPassphraseState(method, device)) {
592
+ // check version
593
+ const support = device.supportNewPassphrase();
594
+ if (!support.support) {
595
+ throw ERRORS.TypedError(
596
+ HardwareErrorCode.DeviceNotSupportPassphrase,
597
+ `Device not support passphrase, please update to ${support.require}`,
598
+ {
599
+ require: support.require,
600
+ }
601
+ );
585
602
  }
586
- )
587
- );
588
- }
589
-
590
- // Check Device passphrase State
591
- const passphraseStateSafety = await device.checkPassphraseStateSafety(
592
- method.payload?.passphraseState,
593
- method.payload?.useEmptyPassphrase,
594
- method.payload?.skipPassphraseCheck
595
- );
596
603
 
597
- // Double check, handles the special case of Touch/Pro
598
- checkPassphraseEnableState(method, device.features);
604
+ // Check Device passphrase State after the Protocol V2 pre-unlock gate.
605
+ const passphraseStateSafety = await device.checkPassphraseStateSafety(
606
+ method.payload?.passphraseState,
607
+ method.payload?.useEmptyPassphrase,
608
+ method.payload?.skipPassphraseCheck,
609
+ hasDeriveCardano(method)
610
+ );
599
611
 
600
- if (!passphraseStateSafety) {
601
- DevicePool.clearDeviceCache(method.payload.connectId);
602
- return Promise.reject(
603
- ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError)
604
- );
605
- }
612
+ // Double check, handles the special case of Touch/Pro
613
+ checkPassphraseEnableState(method, device.features);
606
614
 
607
- // close pin popup window
608
- postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
609
- }
615
+ if (!passphraseStateSafety) {
616
+ DevicePool.clearDeviceCache(method.payload.connectId);
617
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
618
+ }
610
619
 
611
- // Automatic check safety_check level for Kovan, Ropsten, Rinkeby, Goerli test networks.
612
- try {
613
- await method.checkSafetyLevelOnTestNet();
614
- } catch (e) {
615
- const error =
616
- e instanceof HardwareError
617
- ? e
618
- : ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'open safety check failed.');
619
- // messageResponse = createResponseMessage(method.responseID, false, { error });
620
- // requestQueue.resolveRequest(method.responseID, messageResponse);
621
- // return;
622
- throw error;
623
- }
620
+ // Protocol V2 emits the PIN phase completion from DeviceSessionAskPin.
621
+ // Keep the legacy compatibility close for Protocol V1 only.
622
+ if (!device.isProtocolV2()) {
623
+ postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW));
624
+ }
625
+ }
624
626
 
625
- method.device?.commands?.checkDisposed();
627
+ // Automatic check safety_check level for Kovan, Ropsten, Rinkeby, Goerli test networks.
628
+ try {
629
+ await method.checkSafetyLevelOnTestNet();
630
+ } catch (e) {
631
+ throw e instanceof HardwareError
632
+ ? e
633
+ : ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'open safety check failed.');
634
+ }
626
635
 
627
- try {
628
- const response: object = await runMethodWithUnlockRetry(
629
- method,
630
- device,
631
- protocolV2UiCoordinator
632
- );
636
+ method.device?.commands?.checkDisposed();
637
+ },
638
+ });
633
639
  messageResponse = createResponseMessage(method.responseID, true, response);
634
640
  requestQueue.resolveRequest(method.responseID, messageResponse);
635
641
  completeMethodRequestContext(method);
@@ -1279,6 +1285,7 @@ const onDevicePassphraseHandler = async (
1279
1285
  source: requestPayload.source,
1280
1286
  reason: requestPayload.reason,
1281
1287
  expectedPassphraseState: requestPayload.expectedPassphraseState,
1288
+ ...(requestPayload.interaction ? { interaction: requestPayload.interaction } : {}),
1282
1289
  })
1283
1290
  );
1284
1291
  // wait for passphrase
@@ -1308,6 +1315,7 @@ const onEnterPassphraseOnDeviceHandler = (
1308
1315
  passphraseState: device.passphraseState,
1309
1316
  source: requestPayload?.source,
1310
1317
  reason: requestPayload?.reason,
1318
+ ...(requestPayload?.interaction ? { interaction: requestPayload.interaction } : {}),
1311
1319
  })
1312
1320
  );
1313
1321
  };
@@ -1321,10 +1329,39 @@ const onEnterAttachPinOnDeviceHandler = (
1321
1329
  type: 'ButtonRequest_AttachPin',
1322
1330
  source: requestPayload?.source,
1323
1331
  reason: requestPayload?.reason,
1332
+ ...(requestPayload?.interaction ? { interaction: requestPayload.interaction } : {}),
1333
+ })
1334
+ );
1335
+ };
1336
+
1337
+ const onEnterPinOnDeviceHandler = (
1338
+ ...[device, pinType, metadata]: [...DeviceEvents['pin_on_device']]
1339
+ ) => {
1340
+ postMessage(
1341
+ createUiMessage(UI_REQUEST.REQUEST_PIN, {
1342
+ device: device.toMessageObject() as KnownDevice,
1343
+ type:
1344
+ pinType === DeviceSessionPinType.AttachToPin
1345
+ ? 'ButtonRequest_AttachPin'
1346
+ : 'ButtonRequest_PinEntry',
1347
+ source: metadata?.source,
1348
+ reason: metadata?.reason,
1349
+ deviceOnly: metadata?.deviceOnly ?? true,
1350
+ completion: metadata?.completion,
1351
+ method: metadata?.method,
1352
+ page: metadata?.page,
1353
+ operation: metadata?.operation,
1354
+ ...(metadata?.interaction ? { interaction: metadata.interaction } : {}),
1324
1355
  })
1325
1356
  );
1326
1357
  };
1327
1358
 
1359
+ const onPinOnDeviceCompleteHandler = (
1360
+ ...[_, metadata]: [...DeviceEvents['pin_on_device_complete']]
1361
+ ) => {
1362
+ postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_PIN_WINDOW, metadata));
1363
+ };
1364
+
1328
1365
  const onSelectDeviceInBootloaderForWebDeviceHandler = async (
1329
1366
  ...[device, callback]: [...DeviceEvents['select_device_in_bootloader_for_web_device']]
1330
1367
  ) => {
@@ -470,6 +470,7 @@
470
470
  "MessageType_DeviceSessionAskPin": 61202,
471
471
  "MessageType_DeviceSessionAskPassphrase": 61203,
472
472
  "MessageType_PortfolioUpdate": 61400,
473
+ "MessageType_NftUpdate": 61500,
473
474
  "MessageType_OnboardingStatusGet": 61600,
474
475
  "MessageType_OnboardingStatus": 61601
475
476
  },
@@ -12538,6 +12539,15 @@
12538
12539
  }
12539
12540
  }
12540
12541
  },
12542
+ "NftUpdate": {
12543
+ "fields": {
12544
+ "file_name_no_ext": {
12545
+ "rule": "required",
12546
+ "type": "string",
12547
+ "id": 1
12548
+ }
12549
+ }
12550
+ },
12541
12551
  "OnboardingStep": {
12542
12552
  "values": {
12543
12553
  "ONBOARDING_STEP_UNKNOWN": 0,
@@ -74,7 +74,9 @@ import type { DeviceStateReadOptions } from '../types/api/getDeviceState';
74
74
  import type {
75
75
  DeviceButtonRequestPayload,
76
76
  DeviceFeaturesPayload,
77
+ HardwareUiInteractionMeta,
77
78
  PassphraseRequestPayload,
79
+ ProtocolV2UiEventMetadata,
78
80
  } from '../events';
79
81
  import type { PassphrasePromptResponse } from './DeviceCommands';
80
82
  import type { Deferred, HardwareConnectProtocol } from '@onekeyfe/hd-shared';
@@ -110,8 +112,30 @@ const parseRunOptions = (options?: RunOptions): RunOptions => {
110
112
 
111
113
  const Log = getLogger(LoggerNames.Device);
112
114
 
115
+ const isProtocolV2DeviceStatusUnsupportedError = (error: unknown) => {
116
+ if (error instanceof HardwareError) {
117
+ if (error.errorCode === HardwareErrorCode.DeviceNotSupportMethod) {
118
+ return true;
119
+ }
120
+ if (error.params?.failureCode === 'Failure_UnexpectedMessage') {
121
+ return true;
122
+ }
123
+ }
124
+
125
+ const message =
126
+ error instanceof Error
127
+ ? error.message
128
+ : String((error as { message?: unknown } | null)?.message ?? error ?? '');
129
+ return (
130
+ /^Failure_UnexpectedMessage(?:,|\b)/i.test(message) ||
131
+ /\b(?:unsupported message|handler not registered|message handler not found)\b/i.test(message)
132
+ );
133
+ };
134
+
113
135
  export interface DeviceEvents {
114
136
  [DEVICE.PIN]: [Device, PROTO.PinMatrixRequestType | undefined, (err: any, pin: string) => void];
137
+ [DEVICE.PIN_ON_DEVICE]: [Device, DeviceSessionPinType, ProtocolV2UiEventMetadata?];
138
+ [DEVICE.PIN_ON_DEVICE_COMPLETE]: [Device, HardwareUiInteractionMeta];
115
139
  [DEVICE.PASSPHRASE_ON_DEVICE]: [Device, PassphraseRequestPayload?];
116
140
  [DEVICE.ATTACH_PIN_ON_DEVICE]: [Device, PassphraseRequestPayload?];
117
141
  [DEVICE.BUTTON]: [Device, DeviceButtonRequestPayload];
@@ -210,6 +234,24 @@ export class Device extends EventEmitter {
210
234
  /** Force the next initialization to reload DeviceInfo after reconnect or reboot. */
211
235
  private protocolV2StateNeedsReload = false;
212
236
 
237
+ /** Runtime context negotiated once per active Protocol V2 link. */
238
+ private protocolV2RuntimeContext?: ProtocolInfo;
239
+
240
+ /** Coalesces concurrent first-use runtime negotiation on the same active link. */
241
+ private protocolV2RuntimeContextPromise?: Promise<ProtocolInfo>;
242
+
243
+ /** Invalidates an in-flight runtime-context response without adding a transport epoch. */
244
+ private protocolV2RuntimeContextRequestToken?: object;
245
+
246
+ private protocolV2UiInteraction?: {
247
+ interactionId: string;
248
+ phaseCounter: number;
249
+ sequence: number;
250
+ opened: boolean;
251
+ };
252
+
253
+ private protocolV2UiInteractionCounter = 0;
254
+
213
255
  get state() {
214
256
  return this.stateStore.getState();
215
257
  }
@@ -967,13 +1009,7 @@ export class Device extends EventEmitter {
967
1009
  }
968
1010
 
969
1011
  if (refresh.has('status') && !initializedWithDeviceInfo) {
970
- const deviceInfo =
971
- refreshedDeviceInfo ??
972
- (await requestProtocolV2DeviceInfo({
973
- commands: this.commands,
974
- request: getProtocolV2DeviceInfoRequest(),
975
- }));
976
- await this.probeProtocolV2RuntimeState(deviceInfo);
1012
+ await this.probeProtocolV2RuntimeState(refreshedDeviceInfo);
977
1013
  }
978
1014
 
979
1015
  if (refresh.has('settings') && this.state?.status.mode === 'normal') {
@@ -1044,15 +1080,58 @@ export class Device extends EventEmitter {
1044
1080
  return this.features;
1045
1081
  }
1046
1082
 
1047
- async probeProtocolV2RuntimeState(deviceInfo: ProtocolV2DeviceInfo, timeoutMs?: number) {
1048
- const protocolInfo = await requestProtocolV2ProtocolInfo({
1049
- commands: this.commands,
1050
- timeoutMs,
1051
- });
1083
+ async ensureProtocolV2RuntimeContext(timeoutMs?: number): Promise<ProtocolInfo> {
1084
+ const cachedProtocolInfo =
1085
+ this.protocolV2RuntimeContext ??
1086
+ (!this.protocolV2StateNeedsReload
1087
+ ? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
1088
+ : undefined);
1089
+ if (cachedProtocolInfo) {
1090
+ this.protocolV2RuntimeContext = cachedProtocolInfo;
1091
+ return cachedProtocolInfo;
1092
+ }
1093
+
1094
+ if (this.protocolV2RuntimeContextPromise) {
1095
+ return this.protocolV2RuntimeContextPromise;
1096
+ }
1097
+
1098
+ const requestToken = {};
1099
+ const pendingRequest = (async () => {
1100
+ const protocolInfo = await requestProtocolV2ProtocolInfo({
1101
+ commands: this.commands,
1102
+ timeoutMs,
1103
+ });
1104
+ if (this.protocolV2RuntimeContextRequestToken !== requestToken) {
1105
+ throw ERRORS.TypedError(
1106
+ HardwareErrorCode.DeviceInitializeFailed,
1107
+ 'Protocol V2 runtime context was invalidated while loading.'
1108
+ );
1109
+ }
1110
+ this.protocolV2RuntimeContext = protocolInfo;
1111
+ return protocolInfo;
1112
+ })();
1113
+ this.protocolV2RuntimeContextRequestToken = requestToken;
1114
+ this.protocolV2RuntimeContextPromise = pendingRequest;
1115
+
1116
+ try {
1117
+ return await pendingRequest;
1118
+ } finally {
1119
+ if (this.protocolV2RuntimeContextPromise === pendingRequest) {
1120
+ this.protocolV2RuntimeContextPromise = undefined;
1121
+ }
1122
+ if (this.protocolV2RuntimeContextRequestToken === requestToken) {
1123
+ this.protocolV2RuntimeContextRequestToken = undefined;
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ async probeProtocolV2RuntimeState(deviceInfo?: ProtocolV2DeviceInfo, timeoutMs?: number) {
1129
+ const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs);
1052
1130
  const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
1053
- const protocolV2DeviceType = resolveProtocolV2DeviceIdentity(
1054
- deviceInfo.hw?.Device_type
1055
- ).deviceType;
1131
+ const runtimeDeviceInfo = deviceInfo ?? this.state?.raw?.protocolV2DeviceInfo;
1132
+ const protocolV2DeviceType = runtimeDeviceInfo
1133
+ ? resolveProtocolV2DeviceIdentity(runtimeDeviceInfo.hw?.Device_type).deviceType
1134
+ : this.getCurrentDeviceType();
1056
1135
  if (runtimeMode === 'romloader' && protocolV2DeviceType !== EDeviceType.Pro2) {
1057
1136
  throw ERRORS.TypedError(
1058
1137
  HardwareErrorCode.DeviceInitializeFailed,
@@ -1078,10 +1157,18 @@ export class Device extends EventEmitter {
1078
1157
  );
1079
1158
  }
1080
1159
 
1081
- const deviceStatus = await requestProtocolV2DeviceStatus({
1082
- commands: this.commands,
1083
- timeoutMs,
1084
- });
1160
+ let deviceStatus: DeviceStatus;
1161
+ try {
1162
+ deviceStatus = await requestProtocolV2DeviceStatus({
1163
+ commands: this.commands,
1164
+ timeoutMs,
1165
+ });
1166
+ } catch (error) {
1167
+ if (runtimeMode === undefined && isProtocolV2DeviceStatusUnsupportedError(error)) {
1168
+ return this.updateProtocolV2Features(deviceInfo, null, 'bootloader', protocolInfo);
1169
+ }
1170
+ throw error;
1171
+ }
1085
1172
  return this.updateProtocolV2Features(deviceInfo, deviceStatus, 'normal', protocolInfo);
1086
1173
  }
1087
1174
 
@@ -1119,6 +1206,9 @@ export class Device extends EventEmitter {
1119
1206
  this.deviceAcquired = false;
1120
1207
  if (!this.isProtocolV2()) return;
1121
1208
  this.protocolV2StateNeedsReload = true;
1209
+ this.protocolV2RuntimeContext = undefined;
1210
+ this.protocolV2RuntimeContextPromise = undefined;
1211
+ this.protocolV2RuntimeContextRequestToken = undefined;
1122
1212
  this.clearPreInitialized();
1123
1213
  }
1124
1214
 
@@ -1127,9 +1217,14 @@ export class Device extends EventEmitter {
1127
1217
  if (deviceId) {
1128
1218
  deviceWalletSessionStore.deleteDevice(deviceId);
1129
1219
  }
1130
- if (this.isProtocolV2() && this.originalDescriptor.path !== deviceId) {
1131
- deviceWalletSessionStore.deleteDevice(this.originalDescriptor.path);
1220
+ if (this.isProtocolV2()) {
1221
+ if (this.originalDescriptor.path !== deviceId) {
1222
+ deviceWalletSessionStore.deleteDevice(this.originalDescriptor.path);
1223
+ }
1132
1224
  this.protocolV2StateNeedsReload = true;
1225
+ this.protocolV2RuntimeContext = undefined;
1226
+ this.protocolV2RuntimeContextPromise = undefined;
1227
+ this.protocolV2RuntimeContextRequestToken = undefined;
1133
1228
  }
1134
1229
 
1135
1230
  this.passphraseState = undefined;
@@ -1142,6 +1237,9 @@ export class Device extends EventEmitter {
1142
1237
  if (!this.isProtocolV2()) return;
1143
1238
 
1144
1239
  this.protocolV2StateNeedsReload = true;
1240
+ this.protocolV2RuntimeContext = undefined;
1241
+ this.protocolV2RuntimeContextPromise = undefined;
1242
+ this.protocolV2RuntimeContextRequestToken = undefined;
1145
1243
  this.clearPreInitialized();
1146
1244
  let loaderMode: 'bootloader' | 'romloader' | undefined;
1147
1245
  if (rebootType === DeviceRebootType.Bootloader) {
@@ -1494,6 +1592,75 @@ export class Device extends EventEmitter {
1494
1592
  return res.message;
1495
1593
  }
1496
1594
 
1595
+ beginProtocolV2UiInteraction() {
1596
+ if (!this.isProtocolV2()) return;
1597
+ this.protocolV2UiInteraction = {
1598
+ interactionId: `${this.instanceId}:${Date.now()}:${++this.protocolV2UiInteractionCounter}`,
1599
+ phaseCounter: 0,
1600
+ sequence: 0,
1601
+ opened: false,
1602
+ };
1603
+ }
1604
+
1605
+ createProtocolV2UiPhaseMetadata(
1606
+ phase: HardwareUiInteractionMeta['phase'],
1607
+ transition: HardwareUiInteractionMeta['transition'],
1608
+ options?: {
1609
+ phaseId?: string;
1610
+ outcome?: HardwareUiInteractionMeta['outcome'];
1611
+ }
1612
+ ): HardwareUiInteractionMeta | undefined {
1613
+ if (!this.isProtocolV2()) return undefined;
1614
+ if (!this.protocolV2UiInteraction) this.beginProtocolV2UiInteraction();
1615
+
1616
+ const interaction = this.protocolV2UiInteraction;
1617
+ if (!interaction) return undefined;
1618
+ const phaseId =
1619
+ options?.phaseId ?? `${interaction.interactionId}:phase-${++interaction.phaseCounter}`;
1620
+ interaction.opened = true;
1621
+ interaction.sequence += 1;
1622
+
1623
+ return {
1624
+ interactionId: interaction.interactionId,
1625
+ phaseId,
1626
+ sequence: interaction.sequence,
1627
+ phase,
1628
+ transition,
1629
+ ...(options?.outcome ? { outcome: options.outcome } : {}),
1630
+ protocol: 'V2',
1631
+ };
1632
+ }
1633
+
1634
+ completeProtocolV2UiPhase(
1635
+ phase: HardwareUiInteractionMeta,
1636
+ outcome: HardwareUiInteractionMeta['outcome'] = 'succeeded'
1637
+ ) {
1638
+ return this.createProtocolV2UiPhaseMetadata(phase.phase, 'complete', {
1639
+ phaseId: phase.phaseId,
1640
+ outcome,
1641
+ });
1642
+ }
1643
+
1644
+ finishProtocolV2UiInteraction(outcome: HardwareUiInteractionMeta['outcome'] = 'succeeded') {
1645
+ const interaction = this.protocolV2UiInteraction;
1646
+ if (!interaction?.opened) {
1647
+ this.protocolV2UiInteraction = undefined;
1648
+ return undefined;
1649
+ }
1650
+
1651
+ const phaseId = `${interaction.interactionId}:phase-${Math.max(interaction.phaseCounter, 1)}`;
1652
+ const metadata = this.createProtocolV2UiPhaseMetadata('processing', 'finish', {
1653
+ phaseId,
1654
+ outcome,
1655
+ });
1656
+ this.protocolV2UiInteraction = undefined;
1657
+ return metadata;
1658
+ }
1659
+
1660
+ hasOpenProtocolV2UiInteraction() {
1661
+ return this.protocolV2UiInteraction?.opened === true;
1662
+ }
1663
+
1497
1664
  supportUnlockVersionRange(): DeviceFirmwareRange {
1498
1665
  // This range applies to Protocol V1 Pro devices; Pro2 has a dedicated unlock flow.
1499
1666
  return {
@@ -1503,10 +1670,33 @@ export class Device extends EventEmitter {
1503
1670
  };
1504
1671
  }
1505
1672
 
1506
- async unlockDevice(pinType: DeviceSessionPinType = DeviceSessionPinType.Main) {
1673
+ async unlockDevice(
1674
+ pinType?: DeviceSessionPinType,
1675
+ options?: ProtocolV2UiEventMetadata & { emitUiEvent?: boolean }
1676
+ ) {
1507
1677
  if (this.isProtocolV2()) {
1678
+ const requestedPinType = pinType ?? DeviceSessionPinType.Main;
1679
+ const interaction =
1680
+ options?.interaction ??
1681
+ (options?.emitUiEvent === false
1682
+ ? undefined
1683
+ : this.createProtocolV2UiPhaseMetadata('pin', 'start'));
1684
+ if (options?.emitUiEvent !== false) {
1685
+ this.emit(DEVICE.PIN_ON_DEVICE, this, requestedPinType, {
1686
+ source: options?.source ?? 'unlock-coordinator',
1687
+ reason: options?.reason ?? 'device-unlock',
1688
+ deviceOnly: options?.deviceOnly ?? true,
1689
+ completion: options?.completion,
1690
+ method: options?.method,
1691
+ page: options?.page,
1692
+ operation: options?.operation,
1693
+ interaction: options?.interaction ?? interaction,
1694
+ });
1695
+ }
1508
1696
  try {
1509
- await this.commands.typedCall('DeviceSessionAskPin', 'Success', { type: pinType });
1697
+ await this.commands.typedCall('DeviceSessionAskPin', 'Success', {
1698
+ type: requestedPinType,
1699
+ });
1510
1700
  } catch (error) {
1511
1701
  const errorText =
1512
1702
  error instanceof Error
@@ -1518,6 +1708,11 @@ export class Device extends EventEmitter {
1518
1708
  throw error;
1519
1709
  }
1520
1710
 
1711
+ const completion = interaction ? this.completeProtocolV2UiPhase(interaction) : undefined;
1712
+ if (completion) {
1713
+ this.emit(DEVICE.PIN_ON_DEVICE_COMPLETE, this, completion);
1714
+ }
1715
+
1521
1716
  const status = await requestProtocolV2DeviceStatus({ commands: this.commands });
1522
1717
  return this.updateProtocolV2Status(status);
1523
1718
  }
@@ -1579,7 +1774,8 @@ export class Device extends EventEmitter {
1579
1774
  async checkPassphraseStateSafety(
1580
1775
  passphraseState?: string,
1581
1776
  useEmptyPassphrase?: boolean,
1582
- skipPassphraseCheck?: boolean
1777
+ skipPassphraseCheck?: boolean,
1778
+ deriveCardano?: boolean
1583
1779
  ) {
1584
1780
  if (this.isUnacquired()) return false;
1585
1781
 
@@ -1588,6 +1784,7 @@ export class Device extends EventEmitter {
1588
1784
  await getPassphraseStateWithRefreshDeviceInfo(this, {
1589
1785
  expectPassphraseState: expectedPassphraseState,
1590
1786
  onlyMainPin: useEmptyPassphrase,
1787
+ deriveCardano,
1591
1788
  });
1592
1789
 
1593
1790
  // Main wallet and unlock Attach Pin, throw safe error
@@ -1,4 +1,5 @@
1
1
  import type { MessageFactoryFn } from './utils';
2
+ import type { HardwareUiInteractionMeta } from './ui-request';
2
3
  import type { PROTO } from '../constants';
3
4
  import type {
4
5
  KnownDevice as Device,
@@ -26,6 +27,8 @@ export const DEVICE = {
26
27
  // onekey-transport events in protobuf format
27
28
  BUTTON: 'button',
28
29
  PIN: 'pin',
30
+ PIN_ON_DEVICE: 'pin_on_device',
31
+ PIN_ON_DEVICE_COMPLETE: 'pin_on_device_complete',
29
32
  PASSPHRASE: 'passphrase',
30
33
  PASSPHRASE_ON_DEVICE: 'passphrase_on_device',
31
34
  ATTACH_PIN_ON_DEVICE: 'attach_pin_on_device',
@@ -58,6 +61,7 @@ export type PassphraseRequestPayload = {
58
61
  source?: 'wallet-session-coordinator';
59
62
  reason?: 'open-wallet' | 'session-recovery';
60
63
  expectedPassphraseState?: string;
64
+ interaction?: HardwareUiInteractionMeta;
61
65
  };
62
66
 
63
67
  export interface DeviceButtonRequest {
@@ -10,6 +10,7 @@ export const LogBlockEvent: Set<string> = new Set([
10
10
 
11
11
  const LogLabelMethod: Set<string> = new Set([
12
12
  'openWalletSession',
13
+ 'deviceUploadNft',
13
14
  'deviceUploadWallpaper',
14
15
  'uploadPortfolio',
15
16
  'fileWrite',