@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +13 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  4. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
  5. package/__tests__/device-lifecycle-events.test.ts +86 -0
  6. package/__tests__/deviceUploadNft.test.ts +68 -0
  7. package/__tests__/evmSignTypedData.test.ts +42 -0
  8. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
  9. package/__tests__/logBlockEvent.test.ts +45 -122
  10. package/__tests__/open-wallet-session.test.ts +781 -69
  11. package/__tests__/pro2HostAssetPackage.test.ts +165 -0
  12. package/__tests__/protocol-v2-resources.test.ts +7 -4
  13. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  14. package/__tests__/protocol-v2.test.ts +925 -97
  15. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  16. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  18. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  20. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  22. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +2 -2
  24. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  25. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  26. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  28. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  30. package/dist/core/RequestQueue.d.ts +2 -0
  31. package/dist/core/RequestQueue.d.ts.map +1 -1
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/device/Device.d.ts +1 -1
  35. package/dist/device/Device.d.ts.map +1 -1
  36. package/dist/device/DeviceConnector.d.ts +1 -1
  37. package/dist/device/DeviceConnector.d.ts.map +1 -1
  38. package/dist/events/logBlockEvent.d.ts.map +1 -1
  39. package/dist/events/ui-request.d.ts +3 -0
  40. package/dist/events/ui-request.d.ts.map +1 -1
  41. package/dist/index.d.ts +21 -5
  42. package/dist/index.js +2511 -1538
  43. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/settings.d.ts +2 -2
  49. package/dist/types/settings.d.ts.map +1 -1
  50. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  51. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  52. package/dist/utils/patch.d.ts +1 -1
  53. package/dist/utils/patch.d.ts.map +1 -1
  54. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  55. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  56. package/package.json +4 -4
  57. package/src/api/FirmwareUpdateV4.ts +474 -83
  58. package/src/api/OpenWalletSession.ts +39 -12
  59. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  60. package/src/api/evm/EVMSignTypedData.ts +1 -0
  61. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  62. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  63. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  64. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  65. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
  66. package/src/api/protocol-v2/helpers.ts +8 -0
  67. package/src/core/RequestQueue.ts +16 -1
  68. package/src/core/index.ts +164 -26
  69. package/src/data/messages/messages-protocol-v2.json +1589 -1375
  70. package/src/device/Device.ts +3 -1
  71. package/src/device/DeviceConnector.ts +7 -3
  72. package/src/events/logBlockEvent.ts +6 -75
  73. package/src/events/ui-request.ts +3 -0
  74. package/src/protocols/protocol-v2/resources.ts +9 -1
  75. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  76. package/src/protocols/protocol-v2/walletSession.ts +223 -56
  77. package/src/types/settings.ts +4 -2
  78. package/src/utils/deviceFeaturesUtils.ts +4 -0
  79. package/src/utils/pro2HostAssetPackage.ts +400 -0
@@ -2005,7 +2005,8 @@ export class Device extends EventEmitter {
2005
2005
  passphraseState?: string,
2006
2006
  useEmptyPassphrase?: boolean,
2007
2007
  skipPassphraseCheck?: boolean,
2008
- deriveCardano?: boolean
2008
+ deriveCardano?: boolean,
2009
+ mainPinSelected?: boolean
2009
2010
  ) {
2010
2011
  if (this.isUnacquired()) return false;
2011
2012
 
@@ -2016,6 +2017,7 @@ export class Device extends EventEmitter {
2016
2017
  onlyMainPin: useEmptyPassphrase,
2017
2018
  deriveCardano,
2018
2019
  rejectAttachPinForMainWallet: useEmptyPassphrase === true,
2020
+ mainPinSelected,
2019
2021
  });
2020
2022
 
2021
2023
  // Main wallet and unlock Attach Pin, throw safe error
@@ -96,7 +96,8 @@ export default class DeviceConnector {
96
96
  forceCleanRunPromise?: boolean,
97
97
  expectedProtocol?: HardwareConnectProtocol,
98
98
  protocolHint?: HardwareConnectProtocol,
99
- forceProtocolDetection?: boolean
99
+ forceProtocolDetection?: boolean,
100
+ skipProtocolProbe?: boolean
100
101
  ) {
101
102
  Log.debug('acquire', path, session, expectedProtocol, protocolHint);
102
103
  const env = DataManager.getSettings('env');
@@ -104,13 +105,15 @@ export default class DeviceConnector {
104
105
  const transport = this.getActiveTransport();
105
106
  let res;
106
107
  if (DataManager.isBleConnect(env)) {
107
- res = await transport.acquire({
108
+ const acquireInput: Parameters<Transport['acquire']>[0] = {
108
109
  uuid: path,
109
110
  forceCleanRunPromise,
110
111
  expectedProtocol,
111
112
  protocolHint,
112
113
  forceProtocolDetection,
113
- });
114
+ skipProtocolProbe,
115
+ };
116
+ res = await transport.acquire(acquireInput);
114
117
  } else {
115
118
  res = await transport.acquire({
116
119
  path,
@@ -118,6 +121,7 @@ export default class DeviceConnector {
118
121
  expectedProtocol,
119
122
  protocolHint,
120
123
  forceProtocolDetection,
124
+ skipProtocolProbe,
121
125
  });
122
126
  }
123
127
  if (expectedProtocol) {
@@ -8,8 +8,9 @@ export const LogBlockEvent: Set<string> = new Set([
8
8
  UI_RESPONSE.RECEIVE_PASSPHRASE,
9
9
  ]);
10
10
 
11
- const LogLabelMethod: Set<string> = new Set([
12
- 'openWalletSession',
11
+ // These resource/file APIs did not exist in 1.1.32. Skip the payload so
12
+ // the log layer does not copy huge Base64 or binary data.
13
+ const LogPayloadBlockMethod: Set<string> = new Set([
13
14
  'deviceUploadNft',
14
15
  'deviceUploadWallpaper',
15
16
  'uploadPortfolio',
@@ -17,61 +18,6 @@ const LogLabelMethod: Set<string> = new Set([
17
18
  'fileRead',
18
19
  ]);
19
20
 
20
- // 资源上传参数可能包含很大的 Base64 字符串。这里按方法整段跳过,避免日志层
21
- // 递归复制和序列化这些数据;资源 API 与传输内容本身保持不变。
22
- const LogPayloadBlockMethod: Set<string> = new Set([
23
- 'deviceUploadNft',
24
- 'deviceUploadWallpaper',
25
- 'uploadPortfolio',
26
- ]);
27
-
28
- const SensitiveLogKeys: Set<string> = new Set([
29
- 'devicestate',
30
- 'entropy',
31
- 'expectedpassphrasestate',
32
- 'mnemonic',
33
- 'passphrase',
34
- 'passphrasestate',
35
- 'password',
36
- 'pin',
37
- 'privatekey',
38
- 'seed',
39
- 'session',
40
- 'sessionid',
41
- 'walletsessionid',
42
- 'xprv',
43
- ]);
44
-
45
- const normalizeLogKey = (key: string) => key.replace(/[_-]/g, '').toLowerCase();
46
-
47
- const isSigningMethod = (methodName: string) => /sign/i.test(methodName);
48
-
49
- const redactLogValue = (value: unknown, seen: WeakSet<object>): unknown => {
50
- if (ArrayBuffer.isView(value)) {
51
- return `[BINARY:${value.byteLength}]`;
52
- }
53
- if (value instanceof ArrayBuffer) {
54
- return `[BINARY:${value.byteLength}]`;
55
- }
56
- if (Array.isArray(value)) {
57
- return value.map(item => redactLogValue(item, seen));
58
- }
59
- if (!value || typeof value !== 'object') return value;
60
- if (seen.has(value)) return '[CIRCULAR]';
61
-
62
- seen.add(value);
63
- const redacted = Object.fromEntries(
64
- Object.entries(value as Record<string, unknown>).map(([key, item]) => [
65
- key,
66
- SensitiveLogKeys.has(normalizeLogKey(key)) && item !== null && item !== undefined
67
- ? '[REDACTED]'
68
- : redactLogValue(item, seen),
69
- ])
70
- );
71
- seen.delete(value);
72
- return redacted;
73
- };
74
-
75
21
  export function getLogBlockLabel(message: unknown): string | undefined {
76
22
  if (!message || typeof message !== 'object') return undefined;
77
23
 
@@ -86,7 +32,7 @@ export function getLogBlockLabel(message: unknown): string | undefined {
86
32
  }
87
33
 
88
34
  const methodName = method ?? payload?.method;
89
- if (methodName && (LogLabelMethod.has(methodName) || isSigningMethod(methodName))) {
35
+ if (methodName && LogPayloadBlockMethod.has(methodName)) {
90
36
  return methodName;
91
37
  }
92
38
 
@@ -94,25 +40,10 @@ export function getLogBlockLabel(message: unknown): string | undefined {
94
40
  }
95
41
 
96
42
  export function getSafeLogPayload(value: unknown, blockLabel?: string): unknown {
97
- if (
98
- blockLabel &&
99
- (LogBlockEvent.has(blockLabel) ||
100
- LogPayloadBlockMethod.has(blockLabel) ||
101
- isSigningMethod(blockLabel))
102
- ) {
43
+ if (blockLabel) {
103
44
  return { method: blockLabel, payload: '[REDACTED]' };
104
45
  }
105
-
106
- const redactedValue = redactLogValue(value, new WeakSet());
107
- if (
108
- blockLabel &&
109
- redactedValue &&
110
- typeof redactedValue === 'object' &&
111
- !Array.isArray(redactedValue)
112
- ) {
113
- return { ...redactedValue, method: blockLabel };
114
- }
115
- return redactedValue;
46
+ return value;
116
47
  }
117
48
 
118
49
  export function formatLogMethodLabel(label: string, methodName?: string): string {
@@ -189,6 +189,9 @@ export interface FirmwareProgress {
189
189
  totalBytes?: number;
190
190
  rateBytesPerSecond?: number;
191
191
  elapsedMs?: number;
192
+ installTargetId?: number;
193
+ installPhase?: 'prepare' | 'install' | 'verify';
194
+ installPhaseProgress?: number;
192
195
  };
193
196
  }
194
197
 
@@ -59,7 +59,15 @@ export function isProtocolV2ResourceArchiveEntryName(entryName: string): boolean
59
59
  return (
60
60
  fileName.length > 0 &&
61
61
  !fileName.startsWith('.') &&
62
- !parts.some(part => part === '__MACOSX' || part === '.' || part === '..' || part === '')
62
+ !parts.some(
63
+ part =>
64
+ !part ||
65
+ part === '.' ||
66
+ part === '..' ||
67
+ part === '.DS_Store' ||
68
+ part.startsWith('._') ||
69
+ part.toUpperCase() === '__MACOSX'
70
+ )
63
71
  );
64
72
  }
65
73
 
@@ -14,10 +14,14 @@ const Log = getLogger(LoggerNames.Core);
14
14
 
15
15
  export type ProtocolV2UnlockContext = {
16
16
  preflightCompleted: boolean;
17
+ preflightStatusRefreshed?: boolean;
18
+ preflightMainPinSelected?: boolean;
17
19
  };
18
20
 
19
21
  export const createProtocolV2UnlockContext = (): ProtocolV2UnlockContext => ({
20
22
  preflightCompleted: false,
23
+ preflightStatusRefreshed: false,
24
+ preflightMainPinSelected: false,
21
25
  });
22
26
 
23
27
  type RunnableMethod = Pick<
@@ -100,11 +104,12 @@ export async function runMethodWithUnlockPolicy<T = unknown>(
100
104
  await afterStatusBeforeUnlock?.();
101
105
 
102
106
  if (!status.unlocked) {
107
+ const preUnlockPinType = resolvePreUnlockPinType(method);
103
108
  const unlockInteraction: HardwareUiInteractionMeta | undefined = shouldCoordinateUi
104
109
  ? uiCoordinator.enterUnlockInteraction(method.name)
105
110
  : undefined;
106
111
  const unlockedStatus = await device.unlockDevice(
107
- resolvePreUnlockPinType(method),
112
+ preUnlockPinType,
108
113
  shouldCoordinateUi
109
114
  ? { emitUiEvent: false, interaction: unlockInteraction }
110
115
  : {
@@ -120,9 +125,11 @@ export async function runMethodWithUnlockPolicy<T = unknown>(
120
125
  'Protocol V2 device remained locked after the unlock flow.'
121
126
  );
122
127
  }
128
+ context.preflightMainPinSelected = preUnlockPinType === DeviceSessionPinType.Main;
123
129
  Log.debug('Protocol V2 pre-unlock completed', { method: method.name });
124
130
  }
125
131
 
132
+ context.preflightStatusRefreshed = true;
126
133
  context.preflightCompleted = true;
127
134
  }
128
135
 
@@ -63,35 +63,50 @@ const negotiateEventlessWalletSession = async (device: Device) => {
63
63
  const getDeviceSession = async (device: Device, request: DeviceSessionGet) =>
64
64
  device.commands.typedCall('DeviceSessionGet', 'DeviceSession', request);
65
65
 
66
+ const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard];
67
+ const CARDANO_SEED_DOMAINS = [
68
+ DeviceSessionSeedDomain.SeedDomain_Standard,
69
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
70
+ ];
71
+
72
+ const buildDeviceSessionSeedDomains = (deriveCardano?: boolean): DeviceSessionSeedDomain[] =>
73
+ deriveCardano === true ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS;
74
+
75
+ const deviceSessionHasCardano = (message: { seed_domains?: DeviceSessionSeedDomain[] }) =>
76
+ Array.isArray(message.seed_domains) &&
77
+ message.seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano);
78
+
79
+ // origin/dev Get is read/resume only. Seed generation lives on AskPassphrase.
66
80
  const buildDeviceSessionGetRequest = ({
67
81
  sessionId,
68
82
  expectedPassphraseState,
69
- deriveCardano,
70
83
  }: {
71
84
  sessionId?: string;
72
85
  expectedPassphraseState?: string;
73
- deriveCardano?: boolean;
74
86
  } = {}): DeviceSessionGet => ({
75
87
  ...(sessionId ? { session_id: sessionId } : {}),
76
88
  ...(expectedPassphraseState ? { btc_test_address: expectedPassphraseState } : {}),
77
- seed_domains:
78
- deriveCardano === undefined
79
- ? []
80
- : [
81
- DeviceSessionSeedDomain.SeedDomain_Standard,
82
- ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
83
- ],
84
89
  });
85
90
 
86
- const askDevicePassphrase = async (device: Device, requestPayload: DeviceSessionAskPassphrase) => {
87
- await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload);
91
+ const askDevicePassphrase = async (
92
+ device: Device,
93
+ requestPayload: Omit<DeviceSessionAskPassphrase, 'seed_domains'>,
94
+ deriveCardano?: boolean,
95
+ onStatusRefreshed?: () => void
96
+ ) => {
97
+ await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
98
+ ...requestPayload,
99
+ seed_domains: buildDeviceSessionSeedDomains(deriveCardano),
100
+ });
88
101
  await refreshProtocolV2DeviceStatus(device);
102
+ onStatusRefreshed?.();
89
103
  };
90
104
 
91
105
  const selectDeviceSession = async (
92
106
  device: Device,
93
107
  expectedPassphraseState?: string,
94
- deriveCardano?: boolean
108
+ deriveCardano?: boolean,
109
+ onStatusRefreshed?: () => void
95
110
  ) => {
96
111
  const existsAttachPinUser = device.features?.attachToPinEnabled === true;
97
112
  const metadata = {
@@ -153,15 +168,36 @@ const selectDeviceSession = async (
153
168
  emitUiEvent: false,
154
169
  interaction: attachPinInteraction,
155
170
  });
156
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
171
+ onStatusRefreshed?.();
172
+ if (deriveCardano === true) {
173
+ await askDevicePassphrase(
174
+ device,
175
+ { passphrase: '', on_device: false },
176
+ true,
177
+ onStatusRefreshed
178
+ );
179
+ // Firmware AskPassphrase Success clears unlocked_by_attach_to_pin.
180
+ // Identity is still the Attach PIN wallet; keep the SDK flag so a later
181
+ // passphrase picker still locks instead of prompting.
182
+ if (device.features) {
183
+ device.features.unlockedAttachPin = true;
184
+ }
185
+ }
186
+ const attachPinSession = await getDeviceSession(device, buildDeviceSessionGetRequest());
187
+ return Object.assign(attachPinSession, { viaAttachPin: true as const });
157
188
  }
158
189
 
159
190
  if (hasHostPassphrase) {
160
- await askDevicePassphrase(device, {
161
- passphrase: hostPassphrase,
162
- on_device: false,
163
- });
164
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
191
+ await askDevicePassphrase(
192
+ device,
193
+ {
194
+ passphrase: hostPassphrase,
195
+ on_device: false,
196
+ },
197
+ deriveCardano,
198
+ onStatusRefreshed
199
+ );
200
+ return getDeviceSession(device, buildDeviceSessionGetRequest());
165
201
  }
166
202
 
167
203
  const passphraseOnDeviceInteraction = device.createProtocolV2UiPhaseMetadata?.(
@@ -172,8 +208,8 @@ const selectDeviceSession = async (
172
208
  ...metadata,
173
209
  ...(passphraseOnDeviceInteraction ? { interaction: passphraseOnDeviceInteraction } : {}),
174
210
  });
175
- await askDevicePassphrase(device, { on_device: true });
176
- return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
211
+ await askDevicePassphrase(device, { on_device: true }, deriveCardano, onStatusRefreshed);
212
+ return getDeviceSession(device, buildDeviceSessionGetRequest());
177
213
  };
178
214
 
179
215
  export async function getProtocolV2WalletSession(
@@ -191,11 +227,17 @@ export async function getProtocolV2WalletSession(
191
227
  deriveCardano?: boolean;
192
228
  /** Read the wallet already selected by Attach PIN without reopening wallet selection. */
193
229
  readCurrentAttachPinSession?: boolean;
230
+ /** The current call already selected the Main PIN during its unlock preflight. */
231
+ mainPinSelected?: boolean;
194
232
  }
195
233
  ) {
196
234
  const forceWalletSelection =
197
235
  options?.forceWalletSelection === true || options?.initSession === true;
198
236
  const readCurrentAttachPinSession = options?.readCurrentAttachPinSession === true;
237
+ const sessionIsAttachPinWallet = (session?: unknown) =>
238
+ readCurrentAttachPinSession ||
239
+ (session as { viaAttachPin?: boolean } | undefined)?.viaAttachPin === true ||
240
+ device.features?.unlockedAttachPin === true;
199
241
 
200
242
  if (forceWalletSelection) {
201
243
  if (options.onlyMainPin) {
@@ -227,10 +269,20 @@ export async function getProtocolV2WalletSession(
227
269
  : undefined;
228
270
  let response;
229
271
  let resumed = false;
230
- let mainPinSelected =
231
- options?.onlyMainPin === true &&
232
- device.features?.unlocked === true &&
233
- device.features?.unlockedAttachPin === false;
272
+ let walletStatusRefreshed = false;
273
+ const markWalletStatusRefreshed = () => {
274
+ walletStatusRefreshed = true;
275
+ };
276
+ if (options?.onlyMainPin && options.mainPinSelected !== true) {
277
+ await refreshProtocolV2DeviceStatus(device);
278
+ markWalletStatusRefreshed();
279
+ }
280
+ let mainPinAuthenticated =
281
+ options?.mainPinSelected === true ||
282
+ (options?.onlyMainPin === true &&
283
+ device.features?.unlocked === true &&
284
+ device.features?.unlockedAttachPin === false);
285
+ let standardWalletSelected = options?.mainPinSelected === true;
234
286
 
235
287
  const clearCurrentWalletSession = () => {
236
288
  if (options?.onlyMainPin) {
@@ -240,8 +292,41 @@ export async function getProtocolV2WalletSession(
240
292
  }
241
293
  };
242
294
 
295
+ const sessionGetRequest = ({
296
+ sessionId,
297
+ expectedPassphraseState: passphraseState,
298
+ }: {
299
+ sessionId?: string;
300
+ expectedPassphraseState?: string;
301
+ } = {}) =>
302
+ buildDeviceSessionGetRequest({
303
+ sessionId,
304
+ expectedPassphraseState: passphraseState,
305
+ });
306
+
307
+ const askEmptyPassphraseAndGet = async ({
308
+ deriveCardano,
309
+ keepAttachPin,
310
+ }: {
311
+ deriveCardano?: boolean;
312
+ keepAttachPin?: boolean;
313
+ } = {}) => {
314
+ await askDevicePassphrase(
315
+ device,
316
+ { passphrase: '', on_device: false },
317
+ deriveCardano,
318
+ markWalletStatusRefreshed
319
+ );
320
+ if (keepAttachPin && device.features) {
321
+ device.features.unlockedAttachPin = true;
322
+ }
323
+ const session = await getDeviceSession(device, buildDeviceSessionGetRequest());
324
+ return keepAttachPin ? Object.assign(session, { viaAttachPin: true as const }) : session;
325
+ };
326
+
243
327
  const rejectMismatchedAttachPinWallet = async () => {
244
328
  const features = await refreshProtocolV2DeviceStatus(device);
329
+ markWalletStatusRefreshed();
245
330
  if (features.unlockedAttachPin !== true) {
246
331
  return;
247
332
  }
@@ -260,35 +345,63 @@ export async function getProtocolV2WalletSession(
260
345
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
261
346
  };
262
347
 
348
+ // The passphrase picker would prompt. Empty host AskPassphrase does not;
349
+ // that path is Attach PIN / standard Cardano. Switching to a different
350
+ // passphrase wallet still locks first.
351
+ const lockAttachPinBeforePassphraseSelection = async () => {
352
+ if (readCurrentAttachPinSession || options?.onlyMainPin) {
353
+ return;
354
+ }
355
+ if (device.features?.unlockedAttachPin !== true) {
356
+ return;
357
+ }
358
+ await rejectMismatchedAttachPinWallet();
359
+ };
360
+
263
361
  if (options?.onlyMainPin && options.rejectAttachPinForMainWallet) {
264
362
  await rejectMismatchedAttachPinWallet();
265
363
  }
266
364
 
267
365
  const selectMainPin = async (force = false) => {
268
- if (force || !mainPinSelected) {
366
+ if (force || !mainPinAuthenticated) {
269
367
  await device.unlockDevice(DeviceSessionPinType.Main, {
270
368
  source: 'wallet-session-coordinator',
271
369
  reason: expectedPassphraseState ? 'session-recovery' : 'open-wallet',
272
370
  deviceOnly: true,
273
371
  });
274
- mainPinSelected = true;
372
+ markWalletStatusRefreshed();
373
+ mainPinAuthenticated = true;
374
+ standardWalletSelected = true;
275
375
  }
276
376
  };
277
377
 
278
- const selectStandardWallet = async () => {
279
- // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
280
- await selectMainPin();
378
+ const selectStandardWallet = async (forceMainPin = false) => {
281
379
  if (device.features?.passphraseProtection === true) {
282
- await askDevicePassphrase(device, {
283
- passphrase: '',
284
- on_device: false,
285
- });
380
+ // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
381
+ await selectMainPin();
382
+ await askDevicePassphrase(
383
+ device,
384
+ {
385
+ passphrase: '',
386
+ on_device: false,
387
+ },
388
+ options?.deriveCardano,
389
+ markWalletStatusRefreshed
390
+ );
391
+ standardWalletSelected = true;
392
+ } else if (!standardWalletSelected) {
393
+ // Without passphrase protection there is no empty-passphrase selector.
394
+ // An unlocked non-Attach-PIN device is already in the only available wallet context.
395
+ // Force Main PIN only when recovering from a mismatched cached standard session.
396
+ await selectMainPin(forceMainPin);
397
+ standardWalletSelected = true;
286
398
  }
287
399
  };
288
400
 
289
401
  if (readCurrentAttachPinSession) {
290
402
  const status = await requestProtocolV2DeviceStatus(device);
291
403
  device.updateProtocolV2Status(status);
404
+ markWalletStatusRefreshed();
292
405
  if (
293
406
  status.unlocked !== true ||
294
407
  status.passphrase_enabled !== true ||
@@ -297,10 +410,7 @@ export async function getProtocolV2WalletSession(
297
410
  device.clearInternalState();
298
411
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
299
412
  }
300
- response = await getDeviceSession(
301
- device,
302
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
303
- );
413
+ response = await getDeviceSession(device, sessionGetRequest());
304
414
  } else if (options?.onlyMainPin) {
305
415
  expectedPassphraseState = cachedStandardSession?.passphraseState;
306
416
  if (cachedStandardSession) {
@@ -310,10 +420,9 @@ export async function getProtocolV2WalletSession(
310
420
  }
311
421
  response = await getDeviceSession(
312
422
  device,
313
- buildDeviceSessionGetRequest({
423
+ sessionGetRequest({
314
424
  sessionId: cachedStandardSession.sessionId,
315
425
  expectedPassphraseState,
316
- deriveCardano: options?.deriveCardano,
317
426
  })
318
427
  );
319
428
  resumed = true;
@@ -328,19 +437,15 @@ export async function getProtocolV2WalletSession(
328
437
 
329
438
  if (!response) {
330
439
  await selectStandardWallet();
331
- response = await getDeviceSession(
332
- device,
333
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
334
- );
440
+ response = await getDeviceSession(device, sessionGetRequest());
335
441
  }
336
442
  } else if (cachedSessionId && expectedPassphraseState) {
337
443
  try {
338
444
  response = await getDeviceSession(
339
445
  device,
340
- buildDeviceSessionGetRequest({
446
+ sessionGetRequest({
341
447
  sessionId: cachedSessionId,
342
448
  expectedPassphraseState,
343
- deriveCardano: options?.deriveCardano,
344
449
  })
345
450
  );
346
451
  resumed = true;
@@ -350,14 +455,18 @@ export async function getProtocolV2WalletSession(
350
455
  throw error;
351
456
  }
352
457
  resumed = false;
458
+ if (device.features?.unlockedAttachPin === true) {
459
+ // Locking invalidates the old handle, but Attach PIN already selected the wallet.
460
+ // Read the current session and validate its passphrase state below.
461
+ response = await getDeviceSession(device, sessionGetRequest());
462
+ }
353
463
  }
354
464
  } else if (expectedPassphraseState) {
355
465
  try {
356
466
  response = await getDeviceSession(
357
467
  device,
358
- buildDeviceSessionGetRequest({
468
+ sessionGetRequest({
359
469
  expectedPassphraseState,
360
- deriveCardano: options?.deriveCardano,
361
470
  })
362
471
  );
363
472
  } catch (error) {
@@ -372,7 +481,13 @@ export async function getProtocolV2WalletSession(
372
481
  device.clearInternalState();
373
482
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
374
483
  }
375
- response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
484
+ await lockAttachPinBeforePassphraseSelection();
485
+ response = await selectDeviceSession(
486
+ device,
487
+ expectedPassphraseState,
488
+ options?.deriveCardano,
489
+ markWalletStatusRefreshed
490
+ );
376
491
  }
377
492
 
378
493
  let { message } = response;
@@ -395,14 +510,17 @@ export async function getProtocolV2WalletSession(
395
510
  }
396
511
  if (options?.onlyMainPin) {
397
512
  device.clearStandardInternalState?.();
398
- await selectStandardWallet();
399
- response = await getDeviceSession(
400
- device,
401
- buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
402
- );
513
+ await selectStandardWallet(true);
514
+ response = await getDeviceSession(device, sessionGetRequest());
403
515
  } else {
404
516
  device.clearInternalState();
405
- response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
517
+ await lockAttachPinBeforePassphraseSelection();
518
+ response = await selectDeviceSession(
519
+ device,
520
+ expectedPassphraseState,
521
+ options?.deriveCardano,
522
+ markWalletStatusRefreshed
523
+ );
406
524
  }
407
525
  message = response.message;
408
526
  try {
@@ -422,6 +540,54 @@ export async function getProtocolV2WalletSession(
422
540
  }
423
541
  }
424
542
 
543
+ // origin/dev generates Cardano on AskPassphrase, not Get. Empty host
544
+ // passphrase is the Attach PIN / standard-wallet secret. Hidden wallets
545
+ // still need a real passphrase Ask. Passphrase-off Get auto-requests Cardano.
546
+ if (options?.deriveCardano === true && !deviceSessionHasCardano(message)) {
547
+ if (options?.resumeOnly) {
548
+ device.clearInternalState();
549
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
550
+ }
551
+ resumed = false;
552
+ const previousAddress = message.btc_test_address;
553
+ if (sessionIsAttachPinWallet(response)) {
554
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true, keepAttachPin: true });
555
+ } else if (device.features?.passphraseProtection === false) {
556
+ response = await getDeviceSession(device, buildDeviceSessionGetRequest());
557
+ } else if (options?.onlyMainPin) {
558
+ await selectMainPin();
559
+ response = await askEmptyPassphraseAndGet({ deriveCardano: true });
560
+ } else {
561
+ await lockAttachPinBeforePassphraseSelection();
562
+ response = await selectDeviceSession(
563
+ device,
564
+ expectedPassphraseState,
565
+ true,
566
+ markWalletStatusRefreshed
567
+ );
568
+ }
569
+ message = response.message;
570
+ try {
571
+ assertCompleteDeviceSession(message);
572
+ } catch (error) {
573
+ if (options?.onlyMainPin) {
574
+ device.clearStandardInternalState?.();
575
+ } else {
576
+ device.clearInternalState();
577
+ }
578
+ throw error;
579
+ }
580
+ if (previousAddress && previousAddress !== message.btc_test_address) {
581
+ await rejectMismatchedAttachPinWallet();
582
+ clearCurrentWalletSession();
583
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
584
+ }
585
+ if (!deviceSessionHasCardano(message)) {
586
+ clearCurrentWalletSession();
587
+ throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
588
+ }
589
+ }
590
+
425
591
  const internalStateArgs = [
426
592
  true,
427
593
  message.btc_test_address,
@@ -436,9 +602,9 @@ export async function getProtocolV2WalletSession(
436
602
  }
437
603
 
438
604
  let unlockedAttachPin: boolean | undefined;
439
- if (readCurrentAttachPinSession) {
605
+ if (readCurrentAttachPinSession || sessionIsAttachPinWallet(response)) {
440
606
  unlockedAttachPin = true;
441
- } else if (mainPinSelected) {
607
+ } else if (mainPinAuthenticated) {
442
608
  unlockedAttachPin = false;
443
609
  }
444
610
 
@@ -447,6 +613,7 @@ export async function getProtocolV2WalletSession(
447
613
  newSession: message.session_id,
448
614
  unlockedAttachPin,
449
615
  resumed,
616
+ walletStatusRefreshed,
450
617
  };
451
618
  }
452
619