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

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 (108) hide show
  1. package/__tests__/DeviceCommands.test.ts +84 -0
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +31 -0
  3. package/__tests__/core-dispose.test.ts +3 -3
  4. package/__tests__/device-lifecycle-events.test.ts +52 -0
  5. package/__tests__/device-pool-state.test.ts +14 -2
  6. package/__tests__/device-settings.test.ts +25 -0
  7. package/__tests__/device-state-mapper.test.ts +13 -2
  8. package/__tests__/device-state-projector.test.ts +2 -2
  9. package/__tests__/device-utils.test.ts +1 -0
  10. package/__tests__/device-wallet-session-store.test.ts +137 -17
  11. package/__tests__/evmSignTransaction.test.ts +69 -0
  12. package/__tests__/get-device-state.test.ts +78 -30
  13. package/__tests__/kaspaSignTransaction.test.ts +9 -0
  14. package/__tests__/known-device-public-types.type-test.ts +3 -0
  15. package/__tests__/logBlockEvent.test.ts +89 -5
  16. package/__tests__/open-wallet-session.test.ts +499 -60
  17. package/__tests__/pro2Wallpaper.test.ts +1 -1
  18. package/__tests__/protocol-v2.test.ts +1180 -169
  19. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  20. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  21. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  22. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  23. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  24. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  25. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  26. package/dist/api/evm/protocol.d.ts +3 -0
  27. package/dist/api/evm/protocol.d.ts.map +1 -0
  28. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/device/Device.d.ts +10 -4
  32. package/dist/device/Device.d.ts.map +1 -1
  33. package/dist/device/DeviceCommands.d.ts.map +1 -1
  34. package/dist/device/DeviceConnector.d.ts +1 -1
  35. package/dist/device/DeviceConnector.d.ts.map +1 -1
  36. package/dist/device/DevicePool.d.ts +1 -0
  37. package/dist/device/DevicePool.d.ts.map +1 -1
  38. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  39. package/dist/device/DeviceStateProjector.d.ts.map +1 -1
  40. package/dist/device/DeviceWalletSessionStore.d.ts +7 -0
  41. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -1
  42. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  43. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts +7 -0
  44. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -0
  45. package/dist/events/logBlockEvent.d.ts +2 -0
  46. package/dist/events/logBlockEvent.d.ts.map +1 -1
  47. package/dist/index.d.ts +25 -11
  48. package/dist/index.js +1037 -838
  49. package/dist/protocols/protocol-v2/features.d.ts +17 -6
  50. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  51. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  52. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  53. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts +1 -0
  54. package/dist/protocols/protocol-v2/settingsUnlockPolicy.d.ts.map +1 -1
  55. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -1
  56. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -10
  57. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  58. package/dist/types/api/protocolV2.d.ts +2 -2
  59. package/dist/types/api/protocolV2.d.ts.map +1 -1
  60. package/dist/types/device.d.ts +6 -3
  61. package/dist/types/device.d.ts.map +1 -1
  62. package/dist/types/params.d.ts.map +1 -1
  63. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  64. package/dist/utils/deviceFeaturesUtils.d.ts +0 -5
  65. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  66. package/dist/utils/patch.d.ts +1 -1
  67. package/dist/utils/patch.d.ts.map +1 -1
  68. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  69. package/package.json +4 -4
  70. package/src/api/CheckAllFirmwareRelease.ts +11 -81
  71. package/src/api/FirmwareUpdateV4.ts +59 -25
  72. package/src/api/GetPassphraseState.ts +11 -2
  73. package/src/api/OpenWalletSession.ts +16 -8
  74. package/src/api/device/DeviceSettings.ts +8 -1
  75. package/src/api/evm/EVMGetAddress.ts +2 -2
  76. package/src/api/evm/EVMGetPublicKey.ts +2 -2
  77. package/src/api/evm/EVMSignMessage.ts +2 -2
  78. package/src/api/evm/EVMSignTransaction.ts +2 -2
  79. package/src/api/evm/EVMSignTypedData.ts +35 -45
  80. package/src/api/evm/EVMVerifyMessage.ts +2 -2
  81. package/src/api/evm/protocol.ts +6 -0
  82. package/src/api/kaspa/KaspaSignTransaction.ts +7 -0
  83. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +2 -2
  84. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +1 -1
  85. package/src/core/index.ts +7 -5
  86. package/src/data/messages/messages-protocol-v2.json +393 -485
  87. package/src/device/Device.ts +98 -44
  88. package/src/device/DeviceCommands.ts +12 -2
  89. package/src/device/DeviceConnector.ts +10 -7
  90. package/src/device/DevicePool.ts +5 -3
  91. package/src/device/DeviceStateMapper.ts +5 -3
  92. package/src/device/DeviceStateProjector.ts +0 -7
  93. package/src/device/DeviceWalletSessionStore.ts +43 -4
  94. package/src/deviceProfile/buildDeviceFeatures.ts +11 -4
  95. package/src/deviceProfile/protocolV2DeviceIdentity.ts +32 -0
  96. package/src/events/logBlockEvent.ts +84 -1
  97. package/src/protocols/protocol-v2/deviceSession.ts +1 -1
  98. package/src/protocols/protocol-v2/features.ts +58 -16
  99. package/src/protocols/protocol-v2/index.ts +5 -0
  100. package/src/protocols/protocol-v2/settingsUnlockPolicy.ts +8 -3
  101. package/src/protocols/protocol-v2/unlockRetry.ts +18 -7
  102. package/src/protocols/protocol-v2/walletSession.ts +144 -62
  103. package/src/types/api/protocolV2.ts +2 -2
  104. package/src/types/device.ts +8 -2
  105. package/src/types/params.ts +2 -1
  106. package/src/utils/deviceFeaturesCompat.ts +8 -1
  107. package/src/utils/pro2Wallpaper.ts +3 -9
  108. package/tsconfig.type-tests.json +5 -1
@@ -1,10 +1,68 @@
1
+ import { UI_REQUEST } from './ui-request';
1
2
  import { UI_RESPONSE } from './ui-response';
2
3
 
3
4
  export const LogBlockEvent: Set<string> = new Set([
5
+ UI_REQUEST.REQUEST_PASSPHRASE,
6
+ UI_REQUEST.REQUEST_PASSPHRASE_ON_DEVICE,
4
7
  UI_RESPONSE.RECEIVE_PIN,
5
8
  UI_RESPONSE.RECEIVE_PASSPHRASE,
6
9
  ]);
7
10
 
11
+ const LogLabelMethod: Set<string> = new Set([
12
+ 'openWalletSession',
13
+ 'deviceUploadWallpaper',
14
+ 'uploadPortfolio',
15
+ 'fileWrite',
16
+ 'fileRead',
17
+ ]);
18
+
19
+ const SensitiveLogKeys: Set<string> = new Set([
20
+ 'devicestate',
21
+ 'entropy',
22
+ 'expectedpassphrasestate',
23
+ 'mnemonic',
24
+ 'passphrase',
25
+ 'passphrasestate',
26
+ 'password',
27
+ 'pin',
28
+ 'privatekey',
29
+ 'seed',
30
+ 'session',
31
+ 'sessionid',
32
+ 'walletsessionid',
33
+ 'xprv',
34
+ ]);
35
+
36
+ const normalizeLogKey = (key: string) => key.replace(/[_-]/g, '').toLowerCase();
37
+
38
+ const isSigningMethod = (methodName: string) => /sign/i.test(methodName);
39
+
40
+ const redactLogValue = (value: unknown, seen: WeakSet<object>): unknown => {
41
+ if (ArrayBuffer.isView(value)) {
42
+ return `[BINARY:${value.byteLength}]`;
43
+ }
44
+ if (value instanceof ArrayBuffer) {
45
+ return `[BINARY:${value.byteLength}]`;
46
+ }
47
+ if (Array.isArray(value)) {
48
+ return value.map(item => redactLogValue(item, seen));
49
+ }
50
+ if (!value || typeof value !== 'object') return value;
51
+ if (seen.has(value)) return '[CIRCULAR]';
52
+
53
+ seen.add(value);
54
+ const redacted = Object.fromEntries(
55
+ Object.entries(value as Record<string, unknown>).map(([key, item]) => [
56
+ key,
57
+ SensitiveLogKeys.has(normalizeLogKey(key)) && item !== null && item !== undefined
58
+ ? '[REDACTED]'
59
+ : redactLogValue(item, seen),
60
+ ])
61
+ );
62
+ seen.delete(value);
63
+ return redacted;
64
+ };
65
+
8
66
  export function getLogBlockLabel(message: unknown): string | undefined {
9
67
  if (!message || typeof message !== 'object') return undefined;
10
68
 
@@ -19,5 +77,30 @@ export function getLogBlockLabel(message: unknown): string | undefined {
19
77
  }
20
78
 
21
79
  const methodName = method ?? payload?.method;
22
- return methodName;
80
+ if (methodName && (LogLabelMethod.has(methodName) || isSigningMethod(methodName))) {
81
+ return methodName;
82
+ }
83
+
84
+ return undefined;
85
+ }
86
+
87
+ export function getSafeLogPayload(value: unknown, blockLabel?: string): unknown {
88
+ if (blockLabel && (LogBlockEvent.has(blockLabel) || isSigningMethod(blockLabel))) {
89
+ return { method: blockLabel, payload: '[REDACTED]' };
90
+ }
91
+
92
+ const redactedValue = redactLogValue(value, new WeakSet());
93
+ if (
94
+ blockLabel &&
95
+ redactedValue &&
96
+ typeof redactedValue === 'object' &&
97
+ !Array.isArray(redactedValue)
98
+ ) {
99
+ return { ...redactedValue, method: blockLabel };
100
+ }
101
+ return redactedValue;
102
+ }
103
+
104
+ export function formatLogMethodLabel(label: string, methodName?: string): string {
105
+ return methodName ? `${label} [${methodName}]` : label;
23
106
  }
@@ -18,7 +18,7 @@ export function assertCompleteDeviceSession(
18
18
  ) {
19
19
  throw ERRORS.TypedError(
20
20
  HardwareErrorCode.RuntimeError,
21
- 'DeviceSessionGet returned an incomplete DeviceSession response.'
21
+ 'Device returned an incomplete DeviceSession response.'
22
22
  );
23
23
  }
24
24
  }
@@ -4,6 +4,7 @@ import type {
4
4
  DeviceInfoGet,
5
5
  DeviceSEInfo,
6
6
  DeviceStatus,
7
+ ProtocolInfo,
7
8
  ProtocolV2DeviceInfo,
8
9
  } from '@onekeyfe/hd-transport';
9
10
  import type { DeviceCommands } from '../../device/DeviceCommands';
@@ -53,27 +54,54 @@ export const getProtocolV2SeState = (se?: DeviceSEInfo): ProtocolV2SeStateLabel
53
54
  export const getProtocolV2SeType = (se?: DeviceSEInfo): string | null =>
54
55
  normalizeEnumValue(DeviceSeType, se?.type);
55
56
 
56
- export type ProtocolV2RuntimeMode = 'normal' | 'bootloader';
57
+ export type ProtocolV2RuntimeMode = 'normal' | 'bootloader' | 'romloader';
57
58
 
58
59
  /**
59
- * TODO: Once firmware-pro2 exposes ProtocolInfo.build_fingerprint, parse the running
60
- * binary name and remove the temporary DeviceStatusGet-failure heuristic. romloader
61
- * is a separate recovery flow and is excluded until its contract is defined.
62
- *
63
- * The temporary contract has two states: DeviceStatusGet success means normal and
64
- * failure means bootloader. Do not infer mode from application/SE/romloader fields.
60
+ * Protocol V2 fingerprints use:
61
+ * <binary>__<version>__<commit>__<PROD|DEV>__<DEBUG|RELEASE>
65
62
  */
66
- export const getProtocolV2RuntimeMode = ({
67
- deviceInfo: _deviceInfo,
68
- deviceStatusAvailable,
69
- }: {
70
- deviceInfo?: ProtocolV2DeviceInfo | null;
71
- deviceStatusAvailable: boolean;
72
- }): ProtocolV2RuntimeMode => {
73
- if (deviceStatusAvailable) return 'normal';
74
- return 'bootloader';
63
+ export type ProtocolV2BuildFingerprint = {
64
+ binary: 'application' | 'bootloader' | 'romloader';
65
+ version: string;
66
+ commit: string;
67
+ environment: 'PROD' | 'DEV';
68
+ buildType: 'DEBUG' | 'RELEASE';
75
69
  };
76
70
 
71
+ export const parseProtocolV2BuildFingerprint = (
72
+ buildFingerprint: string | null | undefined
73
+ ): ProtocolV2BuildFingerprint | null => {
74
+ if (!buildFingerprint) return null;
75
+ const [binary, version, commit, environment, buildType, ...extra] = buildFingerprint.split('__');
76
+ if (
77
+ extra.length > 0 ||
78
+ (binary !== 'application' && binary !== 'bootloader' && binary !== 'romloader') ||
79
+ !version ||
80
+ !commit ||
81
+ (environment !== 'PROD' && environment !== 'DEV') ||
82
+ (buildType !== 'DEBUG' && buildType !== 'RELEASE')
83
+ ) {
84
+ return null;
85
+ }
86
+ return { binary, version, commit, environment, buildType };
87
+ };
88
+
89
+ export const getProtocolV2RuntimeMode = (
90
+ protocolInfo: ProtocolInfo
91
+ ): ProtocolV2RuntimeMode | undefined => {
92
+ const binary = parseProtocolV2BuildFingerprint(protocolInfo.build_fingerprint)?.binary;
93
+ if (binary === 'application') return 'normal';
94
+ return binary;
95
+ };
96
+
97
+ // MessageType_DeviceStatusGet in the Protocol V2 protobuf registry.
98
+ export const PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE = 60602;
99
+
100
+ export const supportsProtocolV2Message = (
101
+ protocolInfo: ProtocolInfo,
102
+ messageType: number
103
+ ): boolean => protocolInfo.supported_messages.includes(messageType);
104
+
77
105
  export const PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST = {
78
106
  targets: {
79
107
  hw: true,
@@ -131,6 +159,20 @@ export const PROTOCOL_V2_FULL_DEVICE_INFO_REQUEST = {
131
159
  export const PROTOCOL_V2_DEVICE_INFO_REQUEST = PROTOCOL_V2_FULL_DEVICE_INFO_REQUEST;
132
160
  export const PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS = 10 * 1000;
133
161
 
162
+ export async function requestProtocolV2ProtocolInfo({
163
+ commands,
164
+ timeoutMs,
165
+ }: {
166
+ commands: DeviceCommands;
167
+ timeoutMs?: number;
168
+ }): Promise<ProtocolInfo> {
169
+ const response =
170
+ timeoutMs === undefined
171
+ ? await commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {})
172
+ : await commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {}, { timeoutMs });
173
+ return response.message;
174
+ }
175
+
134
176
  export async function requestProtocolV2DeviceInfo({
135
177
  commands,
136
178
  timeoutMs = PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
@@ -1,13 +1,18 @@
1
1
  export {
2
2
  PROTOCOL_V2_DEVICE_INFO_REQUEST,
3
3
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
4
+ PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE,
4
5
  PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST,
5
6
  PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
6
7
  getProtocolV2RuntimeMode,
7
8
  getProtocolV2SeState,
8
9
  getProtocolV2SeType,
10
+ parseProtocolV2BuildFingerprint,
11
+ requestProtocolV2ProtocolInfo,
12
+ supportsProtocolV2Message,
9
13
  } from './features';
10
14
  export type {
15
+ ProtocolV2BuildFingerprint,
11
16
  ProtocolV2DeviceInfo,
12
17
  ProtocolV2FirmwareImageInfo,
13
18
  ProtocolV2SEInfo,
@@ -2,14 +2,19 @@ import type { DeviceSettings, DeviceSettingsPage } from '@onekeyfe/hd-transport'
2
2
  import type { UnlockPolicy } from '../../api/BaseMethod';
3
3
  import type { ProtocolV2InteractionDescriptor } from './uiInteraction';
4
4
 
5
- const LOCK_FREE_DEVICE_SETTINGS = new Set<keyof DeviceSettings>([
5
+ export const PROTOCOL_V2_LOCK_FREE_DEVICE_SETTINGS = [
6
6
  'language',
7
7
  'brightness',
8
+ 'animation_enable',
9
+ 'tap_to_wake',
8
10
  'haptic_feedback',
9
- ]);
11
+ 'device_name_display_enabled',
12
+ ] as const satisfies readonly (keyof DeviceSettings)[];
13
+
14
+ const lockFreeDeviceSettings = new Set<keyof DeviceSettings>(PROTOCOL_V2_LOCK_FREE_DEVICE_SETTINGS);
10
15
 
11
16
  export const getProtocolV2SettingsUnlockPolicy = (settings: DeviceSettings): UnlockPolicy =>
12
- Object.keys(settings).every(key => LOCK_FREE_DEVICE_SETTINGS.has(key as keyof DeviceSettings))
17
+ Object.keys(settings).every(key => lockFreeDeviceSettings.has(key as keyof DeviceSettings))
13
18
  ? 'none'
14
19
  : 'unlock-before-run';
15
20
 
@@ -1,7 +1,7 @@
1
1
  import { LoggerNames, getLogger } from '../../utils';
2
2
  import { isDeviceLockedError } from './lockedError';
3
3
  import { isProtocolV2UiEnabled, resolveProtocolV2UiInteraction } from './uiInteraction';
4
- import { restoreProtocolV2WalletSession } from './walletSession';
4
+ import { refreshProtocolV2DeviceStatus, restoreProtocolV2WalletSession } from './walletSession';
5
5
 
6
6
  import type { BaseMethod } from '../../api/BaseMethod';
7
7
  import type { Device } from '../../device/Device';
@@ -46,8 +46,23 @@ export async function runMethodWithUnlockRetry(
46
46
  uiCoordinator?: UiInteractionCoordinator
47
47
  ) {
48
48
  const shouldEmitUi = isProtocolV2UiEnabled(method);
49
+ const isProtocolV2 = device.isProtocolV2();
50
+ const requiresFreshStatus =
51
+ isProtocolV2 &&
52
+ method.unlockPolicy === 'unlock-before-run' &&
53
+ !device.isBootloader?.() &&
54
+ !device.isRomloader?.();
55
+
56
+ if (requiresFreshStatus) {
57
+ await refreshProtocolV2DeviceStatus(device);
58
+ }
59
+
49
60
  const shouldUnlockBeforeRun =
50
- device.isProtocolV2() && method.unlockPolicy !== 'none' && device.features?.unlocked === false;
61
+ isProtocolV2 &&
62
+ method.unlockPolicy !== 'none' &&
63
+ !device.isBootloader?.() &&
64
+ !device.isRomloader?.() &&
65
+ device.features?.unlocked === false;
51
66
 
52
67
  if (shouldUnlockBeforeRun) {
53
68
  if (shouldEmitUi) {
@@ -68,11 +83,7 @@ export async function runMethodWithUnlockRetry(
68
83
  try {
69
84
  return await method.run();
70
85
  } catch (error) {
71
- if (
72
- !device.isProtocolV2() ||
73
- method.unlockPolicy !== 'retry-on-locked' ||
74
- !isDeviceLockedError(error)
75
- ) {
86
+ if (!isProtocolV2 || method.unlockPolicy !== 'retry-on-locked' || !isDeviceLockedError(error)) {
76
87
  throw error;
77
88
  }
78
89
 
@@ -3,11 +3,31 @@ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import { DEVICE } from '../../events';
5
5
  import { assertCompleteDeviceSession } from './deviceSession';
6
- import { isDeviceLockedError } from './lockedError';
7
6
 
8
- import type { DeviceSessionGet } from '@onekeyfe/hd-transport';
7
+ import type { DeviceSessionAskPassphrase, DeviceSessionGet } from '@onekeyfe/hd-transport';
9
8
  import type { Device } from '../../device/Device';
10
9
 
10
+ const HOST_PASSPHRASE_MAX_BYTES = 50;
11
+
12
+ const isWalletSessionInvalidError = (error: unknown) =>
13
+ typeof error === 'object' &&
14
+ error !== null &&
15
+ 'errorCode' in error &&
16
+ (error as { errorCode?: unknown }).errorCode === HardwareErrorCode.WalletSessionInvalid;
17
+
18
+ const utf8ByteLength = (value: string): number | undefined => {
19
+ let length = 0;
20
+ for (const character of value) {
21
+ const codePoint = character.codePointAt(0) ?? 0;
22
+ if (codePoint >= 0xd800 && codePoint <= 0xdfff) return undefined;
23
+ if (codePoint <= 0x7f) length += 1;
24
+ else if (codePoint <= 0x7ff) length += 2;
25
+ else if (codePoint <= 0xffff) length += 3;
26
+ else length += 4;
27
+ }
28
+ return length;
29
+ };
30
+
11
31
  export async function requestProtocolV2DeviceStatus(device: Device) {
12
32
  const { message } = await device.commands.typedCall('DeviceStatusGet', 'DeviceStatus', {});
13
33
  return message;
@@ -24,41 +44,20 @@ const negotiateEventlessWalletSession = async (device: Device) => {
24
44
  });
25
45
  };
26
46
 
27
- const getDeviceSession = async (device: Device, request: DeviceSessionGet) => {
28
- try {
29
- return await device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
30
- } catch (error) {
31
- if (!isDeviceLockedError(error)) {
32
- throw error;
33
- }
34
- await device.unlockDevice(DeviceSessionPinType.Main);
35
- return device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
36
- }
37
- };
47
+ const getDeviceSession = async (device: Device, request: DeviceSessionGet) =>
48
+ device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
38
49
 
39
- const askDevicePassphrase = async (device: Device, passphrase?: string) => {
40
- const request = () =>
41
- device.commands.typedCall(
42
- 'DeviceSessionAskPassphrase',
43
- 'Success',
44
- passphrase ? { passphrase, on_device: false } : { on_device: true }
45
- );
46
- try {
47
- return await request();
48
- } catch (error) {
49
- if (!isDeviceLockedError(error)) {
50
- throw error;
51
- }
52
- await device.unlockDevice(DeviceSessionPinType.Main);
53
- return request();
54
- }
50
+ const askDevicePassphrase = async (device: Device, requestPayload: DeviceSessionAskPassphrase) => {
51
+ await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload);
52
+ await refreshProtocolV2DeviceStatus(device);
55
53
  };
56
54
 
57
- const selectDeviceSession = async (device: Device) => {
55
+ const selectDeviceSession = async (device: Device, expectedPassphraseState?: string) => {
58
56
  const existsAttachPinUser = device.features?.attachToPinEnabled === true;
59
57
  const metadata = {
60
58
  source: 'wallet-session-coordinator' as const,
61
- reason: 'open-wallet' as const,
59
+ reason: expectedPassphraseState ? ('session-recovery' as const) : ('open-wallet' as const),
60
+ ...(expectedPassphraseState ? { expectedPassphraseState } : {}),
62
61
  };
63
62
  const response = await device.commands.promptPassphrase(
64
63
  {
@@ -68,8 +67,21 @@ const selectDeviceSession = async (device: Device) => {
68
67
  },
69
68
  { cancelDeviceOnReject: false }
70
69
  );
71
- const hasHostPassphrase =
72
- typeof response.passphrase === 'string' && response.passphrase.length > 0;
70
+ const hostPassphrase =
71
+ typeof response.passphrase === 'string' ? response.passphrase.normalize('NFKD') : undefined;
72
+ const hasHostPassphrase = typeof hostPassphrase === 'string' && hostPassphrase.length > 0;
73
+ const hostPassphraseByteLength = hasHostPassphrase ? utf8ByteLength(hostPassphrase) : undefined;
74
+ if (
75
+ hasHostPassphrase &&
76
+ (hostPassphrase.includes('\0') ||
77
+ hostPassphraseByteLength === undefined ||
78
+ hostPassphraseByteLength > HOST_PASSPHRASE_MAX_BYTES)
79
+ ) {
80
+ throw ERRORS.TypedError(
81
+ HardwareErrorCode.RuntimeError,
82
+ `Host passphrase must contain 1 to ${HOST_PASSPHRASE_MAX_BYTES} valid UTF-8 bytes without NUL.`
83
+ );
84
+ }
73
85
  const selections = [
74
86
  hasHostPassphrase,
75
87
  response.passphraseOnDevice === true,
@@ -96,26 +108,38 @@ const selectDeviceSession = async (device: Device) => {
96
108
  }
97
109
 
98
110
  if (hasHostPassphrase) {
99
- await askDevicePassphrase(device, response.passphrase);
111
+ await askDevicePassphrase(device, {
112
+ passphrase: hostPassphrase,
113
+ on_device: false,
114
+ });
100
115
  return getDeviceSession(device, {});
101
116
  }
102
117
 
103
118
  device.emit(DEVICE.PASSPHRASE_ON_DEVICE, device, metadata);
104
- await askDevicePassphrase(device);
119
+ await askDevicePassphrase(device, { on_device: true });
105
120
  return getDeviceSession(device, {});
106
121
  };
107
122
 
108
123
  export async function getProtocolV2WalletSession(
109
124
  device: Device,
110
125
  options?: {
126
+ forceWalletSelection?: boolean;
127
+ /** @deprecated 兼容旧 getPassphraseState;新流程使用 forceWalletSelection。 */
111
128
  initSession?: boolean;
112
129
  expectedPassphraseState?: string;
113
130
  onlyMainPin?: boolean;
114
131
  resumeOnly?: boolean;
115
132
  }
116
133
  ) {
117
- if (options?.initSession) {
118
- device.clearInternalState();
134
+ const forceWalletSelection =
135
+ options?.forceWalletSelection === true || options?.initSession === true;
136
+
137
+ if (forceWalletSelection) {
138
+ if (options.onlyMainPin) {
139
+ device.clearStandardInternalState?.();
140
+ } else {
141
+ device.clearInternalState();
142
+ }
119
143
  device.passphraseState = undefined;
120
144
  }
121
145
 
@@ -124,35 +148,54 @@ export async function getProtocolV2WalletSession(
124
148
  if (options?.onlyMainPin) {
125
149
  device.passphraseState = undefined;
126
150
  }
127
- const expectedPassphraseState = options?.onlyMainPin
151
+ let expectedPassphraseState = options?.onlyMainPin
128
152
  ? undefined
129
153
  : options?.expectedPassphraseState ?? device.passphraseState;
130
154
 
131
- if (device.features?.unlocked === false) {
132
- await device.unlockDevice(DeviceSessionPinType.Main);
133
- }
134
-
135
- if (options?.onlyMainPin || device.getCurrentPassphraseProtection() === false) {
136
- return {
137
- passphraseState: undefined,
138
- newSession: undefined,
139
- unlockedAttachPin: device.features?.unlockedAttachPin ?? false,
140
- resumed: false,
141
- };
142
- }
143
-
155
+ const cachedStandardSession = options?.onlyMainPin
156
+ ? device.getStandardInternalState?.()
157
+ : undefined;
144
158
  const cachedSessionId =
145
- typeof device.getInternalState === 'function' ? device.getInternalState() : undefined;
159
+ !options?.onlyMainPin && typeof device.getInternalState === 'function'
160
+ ? device.getInternalState()
161
+ : undefined;
146
162
  let response;
147
163
  let resumed = false;
148
164
 
149
- if (cachedSessionId && expectedPassphraseState) {
165
+ if (options?.onlyMainPin) {
166
+ expectedPassphraseState = cachedStandardSession?.passphraseState;
167
+ if (cachedStandardSession) {
168
+ try {
169
+ if (device.features?.unlockedAttachPin === true) {
170
+ await device.unlockDevice(DeviceSessionPinType.Main);
171
+ }
172
+ response = await getDeviceSession(device, {
173
+ session_id: cachedStandardSession.sessionId,
174
+ });
175
+ resumed = true;
176
+ } catch (error) {
177
+ device.clearStandardInternalState?.();
178
+ if (!isWalletSessionInvalidError(error)) {
179
+ throw error;
180
+ }
181
+ resumed = false;
182
+ }
183
+ }
184
+
185
+ if (!response) {
186
+ await device.unlockDevice(DeviceSessionPinType.Main);
187
+ response = await getDeviceSession(device, {});
188
+ }
189
+ } else if (cachedSessionId && expectedPassphraseState) {
150
190
  try {
151
191
  response = await getDeviceSession(device, { session_id: cachedSessionId });
152
192
  resumed = true;
153
193
  } catch (error) {
154
194
  device.clearInternalState();
155
- throw error;
195
+ if (options?.resumeOnly || !isWalletSessionInvalidError(error)) {
196
+ throw error;
197
+ }
198
+ resumed = false;
156
199
  }
157
200
  }
158
201
 
@@ -161,28 +204,67 @@ export async function getProtocolV2WalletSession(
161
204
  device.clearInternalState();
162
205
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
163
206
  }
164
- response = await selectDeviceSession(device);
207
+ response = await selectDeviceSession(device, expectedPassphraseState);
165
208
  }
166
209
 
167
- const { message } = response;
210
+ let { message } = response;
168
211
  try {
169
212
  assertCompleteDeviceSession(message);
170
213
  } catch (error) {
171
- device.clearInternalState();
214
+ if (options?.onlyMainPin) {
215
+ device.clearStandardInternalState?.();
216
+ } else {
217
+ device.clearInternalState();
218
+ }
172
219
  throw error;
173
220
  }
174
221
  if (expectedPassphraseState && expectedPassphraseState !== message.btc_test_address) {
175
- device.clearInternalState();
176
- throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
222
+ resumed = false;
223
+ if (options?.resumeOnly) {
224
+ device.clearInternalState();
225
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
226
+ }
227
+ if (options?.onlyMainPin) {
228
+ device.clearStandardInternalState?.();
229
+ await device.unlockDevice(DeviceSessionPinType.Main);
230
+ response = await getDeviceSession(device, {});
231
+ } else {
232
+ device.clearInternalState();
233
+ response = await selectDeviceSession(device, expectedPassphraseState);
234
+ }
235
+ message = response.message;
236
+ try {
237
+ assertCompleteDeviceSession(message);
238
+ } catch (error) {
239
+ if (options?.onlyMainPin) {
240
+ device.clearStandardInternalState?.();
241
+ } else {
242
+ device.clearInternalState();
243
+ }
244
+ throw error;
245
+ }
246
+ if (expectedPassphraseState !== message.btc_test_address) {
247
+ if (options?.onlyMainPin) {
248
+ device.clearStandardInternalState?.();
249
+ } else {
250
+ device.clearInternalState();
251
+ }
252
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
253
+ }
177
254
  }
178
255
 
179
- device.updateInternalState(
256
+ const internalStateArgs = [
180
257
  true,
181
258
  message.btc_test_address,
182
259
  device.getCurrentDeviceId(),
183
260
  message.session_id,
184
- options?.initSession ? null : cachedSessionId ?? null
185
- );
261
+ forceWalletSelection ? null : cachedSessionId ?? null,
262
+ ] as const;
263
+ if (options?.onlyMainPin) {
264
+ device.updateInternalState(...internalStateArgs, 'standard');
265
+ } else {
266
+ device.updateInternalState(...internalStateArgs);
267
+ }
186
268
 
187
269
  return {
188
270
  passphraseState: message.btc_test_address,
@@ -1,5 +1,5 @@
1
1
  import type { CommonParams, Response } from '../params';
2
- import type { DevOnboardingStatus, Success } from '@onekeyfe/hd-transport';
2
+ import type { OnboardingStatus, Success } from '@onekeyfe/hd-transport';
3
3
  import type { DeviceRebootParams } from '../../api/protocol-v2/helpers';
4
4
  import type {
5
5
  DeviceUploadWallpaperParams,
@@ -45,7 +45,7 @@ export declare function deviceReboot(
45
45
  export declare function deviceGetOnboardingStatus(
46
46
  connectId: string,
47
47
  params?: CommonParams
48
- ): Response<DevOnboardingStatus>;
48
+ ): Response<OnboardingStatus>;
49
49
 
50
50
  export declare function deviceUploadWallpaper(
51
51
  connectId: string,
@@ -1,4 +1,4 @@
1
- import { EDeviceType, type EFirmwareType } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, type EFirmwareType, type HardwareConnectProtocol } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import type { IVersionArray } from './settings';
4
4
  import type { PROTO } from '../constants';
@@ -7,6 +7,7 @@ import type {
7
7
  OneKeyDeviceCommType,
8
8
  ProtocolV2DeviceInfo,
9
9
  DeviceStatus as ProtocolV2DeviceStatus,
10
+ ProtocolInfo as ProtocolV2ProtocolInfo,
10
11
  } from '@onekeyfe/hd-transport';
11
12
 
12
13
  export type DeviceStatus = 'available' | 'occupied' | 'used';
@@ -28,6 +29,8 @@ export type UnavailableCapabilities = { [key: string]: UnavailableCapability };
28
29
 
29
30
  export type KnownDevice = {
30
31
  connectId: string | null;
32
+ /** Protocol family confirmed by the transport's active probe. */
33
+ connectProtocol?: HardwareConnectProtocol;
31
34
  /**
32
35
  * Stable physical-device identity. Current SDK responses always include this
33
36
  * value; it remains optional in the type during the uuid compatibility window.
@@ -46,7 +49,7 @@ export type KnownDevice = {
46
49
  /** Current SDK/transport usage state for connection indicators. */
47
50
  status?: DeviceStatus;
48
51
  mode: EOneKeyDeviceMode;
49
- features?: Features;
52
+ features: Features;
50
53
  /** Unified SDK device-state snapshot; features remains a legacy projection. */
51
54
  state?: DeviceState;
52
55
  unavailableCapabilities: UnavailableCapabilities;
@@ -61,6 +64,8 @@ export type KnownDevice = {
61
64
 
62
65
  export type SearchDevice = {
63
66
  connectId: string | null;
67
+ /** Protocol family confirmed by the transport's active probe. */
68
+ connectProtocol?: HardwareConnectProtocol;
64
69
  /**
65
70
  * Available after device initialization. BLE discovery does not connect to the
66
71
  * device, so it returns null until a later initialized device response. Current
@@ -155,6 +160,7 @@ export type DeviceFeaturesVerify = {
155
160
  export type DeviceFeaturesRaw = {
156
161
  protocolV1Features?: PROTO.Features;
157
162
  protocolV1OneKeyFeatures?: OnekeyFeatures;
163
+ protocolV2ProtocolInfo?: ProtocolV2ProtocolInfo;
158
164
  protocolV2DeviceInfo?: ProtocolV2DeviceInfo;
159
165
  protocolV2DeviceStatus?: ProtocolV2DeviceStatus;
160
166
  };
@@ -60,7 +60,8 @@ export interface CommonParams {
60
60
  usePreInitialize?: boolean;
61
61
 
62
62
  /**
63
- * Expected transport protocol. If omitted, SDK probes Protocol V1 then Protocol V2.
63
+ * Strictly expected transport protocol. The SDK actively verifies this value and
64
+ * rejects a mismatch. If omitted, a cached protocol only influences probe order.
64
65
  */
65
66
  connectProtocol?: HardwareConnectProtocol;
66
67
  }