@onekeyfe/hd-core 1.2.0-alpha.183 → 1.2.0-alpha.184

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 (30) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +13 -0
  3. package/__tests__/device-lifecycle-events.test.ts +19 -0
  4. package/__tests__/open-wallet-session.test.ts +127 -1
  5. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  6. package/__tests__/protocol-v2.test.ts +50 -0
  7. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  8. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  9. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/device/Device.d.ts +1 -1
  13. package/dist/device/Device.d.ts.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +103 -35
  16. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  17. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  18. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  19. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  20. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  21. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  22. package/package.json +4 -4
  23. package/src/api/FirmwareUpdateV4.ts +25 -8
  24. package/src/api/OpenWalletSession.ts +39 -9
  25. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  26. package/src/core/index.ts +16 -2
  27. package/src/device/Device.ts +3 -1
  28. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  29. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  30. package/src/utils/deviceFeaturesUtils.ts +4 -0
@@ -29,6 +29,12 @@ const wasResumed = (session: unknown) =>
29
29
  'resumed' in session &&
30
30
  (session as { resumed?: unknown }).resumed === true;
31
31
 
32
+ const wasWalletStatusRefreshed = (session: unknown) =>
33
+ !!session &&
34
+ typeof session === 'object' &&
35
+ 'walletStatusRefreshed' in session &&
36
+ (session as { walletStatusRefreshed?: unknown }).walletStatusRefreshed === true;
37
+
32
38
  const requireHiddenWalletResponse = (session: { passphraseState?: string }) => {
33
39
  if (!session.passphraseState) {
34
40
  throw ERRORS.TypedError(
@@ -87,12 +93,20 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
87
93
  this.unlockPolicy = 'unlock-before-run';
88
94
  this.skipForceUpdateCheck = true;
89
95
  this.params = normalizeParams(this.payload as unknown as Record<string, unknown>);
96
+ this.protocolV2PreUnlockPinType =
97
+ this.params.mode === OpenWalletSessionMode.SelectHidden
98
+ ? DeviceSessionPinType.Any
99
+ : DeviceSessionPinType.Main;
90
100
  this.payload.useEmptyPassphrase = this.params.mode === OpenWalletSessionMode.Standard;
91
101
  }
92
102
 
93
103
  async run(): Promise<OpenWalletSessionPayload> {
94
104
  const isProtocolV2 = this.device.isProtocolV2();
95
- let state = await this.device.getDeviceState({ refreshSections: ['status'] });
105
+ const reusePreflightStatus =
106
+ isProtocolV2 && this.protocolV2UnlockContext?.preflightStatusRefreshed === true;
107
+ let state = reusePreflightStatus
108
+ ? await this.device.getDeviceState()
109
+ : await this.device.getDeviceState({ refreshSections: ['status'] });
96
110
  let currentDeviceId = state.identity.deviceId;
97
111
  const hasAuthoritativeProtocolV2WalletStatus = (candidate: typeof state) =>
98
112
  candidate.status.unlocked === true &&
@@ -115,14 +129,25 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
115
129
  currentDeviceId = state.identity.deviceId;
116
130
  return state;
117
131
  };
132
+ const resolveProtocolV2DeviceStateAfterSession = async (session: unknown) => {
133
+ if (!wasWalletStatusRefreshed(session)) {
134
+ return refreshProtocolV2DeviceState();
135
+ }
136
+ state = await this.device.getDeviceState();
137
+ currentDeviceId = state.identity.deviceId;
138
+ return state;
139
+ };
118
140
  const ensureProtocolV2WalletStatus = async () => {
119
141
  if (isProtocolV2 && !hasAuthoritativeProtocolV2WalletStatus(state)) {
120
- await this.device.unlockDevice(DeviceSessionPinType.Main, {
121
- source: 'unlock-coordinator',
122
- reason: 'device-locked',
123
- deviceOnly: true,
124
- method: 'openWalletSession',
125
- });
142
+ await this.device.unlockDevice(
143
+ this.protocolV2PreUnlockPinType ?? DeviceSessionPinType.Main,
144
+ {
145
+ source: 'unlock-coordinator',
146
+ reason: 'device-locked',
147
+ deviceOnly: true,
148
+ method: 'openWalletSession',
149
+ }
150
+ );
126
151
  requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState());
127
152
  }
128
153
  return state;
@@ -139,13 +164,16 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
139
164
  selectMainWalletBeforeRestore:
140
165
  !hasAuthoritativeProtocolV2WalletStatus(state) ||
141
166
  state.status.unlockedAttachPin === true,
167
+ mainPinSelected: this.protocolV2UnlockContext?.preflightMainPinSelected,
142
168
  })
143
169
  : await getPassphraseStateWithRefreshDeviceInfo(this.device, {
144
170
  onlyMainPin: true,
145
171
  initSession: this.payload.initSession,
146
172
  });
147
173
  const refreshedState = isProtocolV2
148
- ? requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState())
174
+ ? requireAuthoritativeProtocolV2WalletStatus(
175
+ await resolveProtocolV2DeviceStateAfterSession(session)
176
+ )
149
177
  : state;
150
178
  const deviceId = requireDeviceId();
151
179
  if (
@@ -233,7 +261,9 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
233
261
  })
234
262
  : await getPassphraseStateWithRefreshDeviceInfo(this.device, { initSession: true });
235
263
  const refreshedState = isProtocolV2
236
- ? requireAuthoritativeProtocolV2WalletStatus(await refreshProtocolV2DeviceState())
264
+ ? requireAuthoritativeProtocolV2WalletStatus(
265
+ await resolveProtocolV2DeviceStateAfterSession(session)
266
+ )
237
267
  : state;
238
268
  const deviceId = requireDeviceId();
239
269
  if (isProtocolV2 && refreshedState.status.passphraseProtection !== true) {
@@ -400,7 +400,8 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
400
400
  this.payload.passphraseState,
401
401
  useEmptyPassphrase,
402
402
  this.payload.skipPassphraseCheck,
403
- deriveCardano
403
+ deriveCardano,
404
+ this.protocolV2UnlockContext?.preflightMainPinSelected
404
405
  );
405
406
  if (!passphraseStateSafety) {
406
407
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
package/src/core/index.ts CHANGED
@@ -651,7 +651,8 @@ const onCallDevice = async (
651
651
  method.payload?.passphraseState,
652
652
  method.payload?.useEmptyPassphrase,
653
653
  method.payload?.skipPassphraseCheck,
654
- hasDeriveCardano(method)
654
+ hasDeriveCardano(method),
655
+ method.protocolV2UnlockContext?.preflightMainPinSelected
655
656
  );
656
657
 
657
658
  // Double check, handles the special case of Touch/Pro
@@ -957,6 +958,14 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
957
958
  );
958
959
  }
959
960
 
961
+ export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
962
+ return (
963
+ method.payload.connectProtocol === 'V2' &&
964
+ (error as { errorCode?: unknown })?.errorCode ===
965
+ HardwareErrorCode.BlePeerRemovedPairingInformation
966
+ );
967
+ }
968
+
960
969
  /**
961
970
  * If the Bluetooth connection times out, retry up to 6 times
962
971
  * @param retryCount - Current retry count (default 0)
@@ -1258,7 +1267,8 @@ const ensureConnected = async (
1258
1267
  HardwareErrorCode.BridgeNeedsPermission,
1259
1268
  HardwareErrorCode.DeviceInterruptedFromUser,
1260
1269
  HardwareErrorCode.CallQueueActionCancelled,
1261
- ].includes(error.errorCode)
1270
+ ].includes(error.errorCode) ||
1271
+ isProtocolV2PeerRemovedPairingError(method, error)
1262
1272
  ) {
1263
1273
  reject(error);
1264
1274
  return;
@@ -1425,6 +1435,10 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) =>
1425
1435
  const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
1426
1436
  if (!method.useDevicePassphraseState) return false;
1427
1437
 
1438
+ if (device.isProtocolV2() && method.payload?.useEmptyPassphrase === true) {
1439
+ return true;
1440
+ }
1441
+
1428
1442
  return device.hasUsePassphrase();
1429
1443
  };
1430
1444
 
@@ -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
@@ -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
 
@@ -83,15 +83,21 @@ const buildDeviceSessionGetRequest = ({
83
83
  ],
84
84
  });
85
85
 
86
- const askDevicePassphrase = async (device: Device, requestPayload: DeviceSessionAskPassphrase) => {
86
+ const askDevicePassphrase = async (
87
+ device: Device,
88
+ requestPayload: DeviceSessionAskPassphrase,
89
+ onStatusRefreshed?: () => void
90
+ ) => {
87
91
  await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload);
88
92
  await refreshProtocolV2DeviceStatus(device);
93
+ onStatusRefreshed?.();
89
94
  };
90
95
 
91
96
  const selectDeviceSession = async (
92
97
  device: Device,
93
98
  expectedPassphraseState?: string,
94
- deriveCardano?: boolean
99
+ deriveCardano?: boolean,
100
+ onStatusRefreshed?: () => void
95
101
  ) => {
96
102
  const existsAttachPinUser = device.features?.attachToPinEnabled === true;
97
103
  const metadata = {
@@ -153,14 +159,19 @@ const selectDeviceSession = async (
153
159
  emitUiEvent: false,
154
160
  interaction: attachPinInteraction,
155
161
  });
162
+ onStatusRefreshed?.();
156
163
  return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
157
164
  }
158
165
 
159
166
  if (hasHostPassphrase) {
160
- await askDevicePassphrase(device, {
161
- passphrase: hostPassphrase,
162
- on_device: false,
163
- });
167
+ await askDevicePassphrase(
168
+ device,
169
+ {
170
+ passphrase: hostPassphrase,
171
+ on_device: false,
172
+ },
173
+ onStatusRefreshed
174
+ );
164
175
  return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
165
176
  }
166
177
 
@@ -172,7 +183,7 @@ const selectDeviceSession = async (
172
183
  ...metadata,
173
184
  ...(passphraseOnDeviceInteraction ? { interaction: passphraseOnDeviceInteraction } : {}),
174
185
  });
175
- await askDevicePassphrase(device, { on_device: true });
186
+ await askDevicePassphrase(device, { on_device: true }, onStatusRefreshed);
176
187
  return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
177
188
  };
178
189
 
@@ -191,6 +202,8 @@ export async function getProtocolV2WalletSession(
191
202
  deriveCardano?: boolean;
192
203
  /** Read the wallet already selected by Attach PIN without reopening wallet selection. */
193
204
  readCurrentAttachPinSession?: boolean;
205
+ /** The current call already selected the Main PIN during its unlock preflight. */
206
+ mainPinSelected?: boolean;
194
207
  }
195
208
  ) {
196
209
  const forceWalletSelection =
@@ -227,10 +240,16 @@ export async function getProtocolV2WalletSession(
227
240
  : undefined;
228
241
  let response;
229
242
  let resumed = false;
230
- let mainPinSelected =
231
- options?.onlyMainPin === true &&
232
- device.features?.unlocked === true &&
233
- device.features?.unlockedAttachPin === false;
243
+ let walletStatusRefreshed = false;
244
+ const markWalletStatusRefreshed = () => {
245
+ walletStatusRefreshed = true;
246
+ };
247
+ let mainPinAuthenticated =
248
+ options?.mainPinSelected === true ||
249
+ (options?.onlyMainPin === true &&
250
+ device.features?.unlocked === true &&
251
+ device.features?.unlockedAttachPin === false);
252
+ let standardWalletSelected = options?.mainPinSelected === true;
234
253
 
235
254
  const clearCurrentWalletSession = () => {
236
255
  if (options?.onlyMainPin) {
@@ -242,6 +261,7 @@ export async function getProtocolV2WalletSession(
242
261
 
243
262
  const rejectMismatchedAttachPinWallet = async () => {
244
263
  const features = await refreshProtocolV2DeviceStatus(device);
264
+ markWalletStatusRefreshed();
245
265
  if (features.unlockedAttachPin !== true) {
246
266
  return;
247
267
  }
@@ -265,30 +285,42 @@ export async function getProtocolV2WalletSession(
265
285
  }
266
286
 
267
287
  const selectMainPin = async (force = false) => {
268
- if (force || !mainPinSelected) {
288
+ if (force || !mainPinAuthenticated) {
269
289
  await device.unlockDevice(DeviceSessionPinType.Main, {
270
290
  source: 'wallet-session-coordinator',
271
291
  reason: expectedPassphraseState ? 'session-recovery' : 'open-wallet',
272
292
  deviceOnly: true,
273
293
  });
274
- mainPinSelected = true;
294
+ markWalletStatusRefreshed();
295
+ mainPinAuthenticated = true;
296
+ standardWalletSelected = true;
275
297
  }
276
298
  };
277
299
 
278
300
  const selectStandardWallet = async () => {
279
- // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
280
- await selectMainPin();
281
301
  if (device.features?.passphraseProtection === true) {
282
- await askDevicePassphrase(device, {
283
- passphrase: '',
284
- on_device: false,
285
- });
302
+ // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
303
+ await selectMainPin();
304
+ await askDevicePassphrase(
305
+ device,
306
+ {
307
+ passphrase: '',
308
+ on_device: false,
309
+ },
310
+ markWalletStatusRefreshed
311
+ );
312
+ standardWalletSelected = true;
313
+ } else if (!standardWalletSelected) {
314
+ // Without passphrase protection there is no empty-passphrase selector.
315
+ // Main PIN selection is the only authoritative switch back to the standard wallet.
316
+ await selectMainPin(true);
286
317
  }
287
318
  };
288
319
 
289
320
  if (readCurrentAttachPinSession) {
290
321
  const status = await requestProtocolV2DeviceStatus(device);
291
322
  device.updateProtocolV2Status(status);
323
+ markWalletStatusRefreshed();
292
324
  if (
293
325
  status.unlocked !== true ||
294
326
  status.passphrase_enabled !== true ||
@@ -372,7 +404,12 @@ export async function getProtocolV2WalletSession(
372
404
  device.clearInternalState();
373
405
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
374
406
  }
375
- response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
407
+ response = await selectDeviceSession(
408
+ device,
409
+ expectedPassphraseState,
410
+ options?.deriveCardano,
411
+ markWalletStatusRefreshed
412
+ );
376
413
  }
377
414
 
378
415
  let { message } = response;
@@ -402,7 +439,12 @@ export async function getProtocolV2WalletSession(
402
439
  );
403
440
  } else {
404
441
  device.clearInternalState();
405
- response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
442
+ response = await selectDeviceSession(
443
+ device,
444
+ expectedPassphraseState,
445
+ options?.deriveCardano,
446
+ markWalletStatusRefreshed
447
+ );
406
448
  }
407
449
  message = response.message;
408
450
  try {
@@ -438,7 +480,7 @@ export async function getProtocolV2WalletSession(
438
480
  let unlockedAttachPin: boolean | undefined;
439
481
  if (readCurrentAttachPinSession) {
440
482
  unlockedAttachPin = true;
441
- } else if (mainPinSelected) {
483
+ } else if (mainPinAuthenticated) {
442
484
  unlockedAttachPin = false;
443
485
  }
444
486
 
@@ -447,6 +489,7 @@ export async function getProtocolV2WalletSession(
447
489
  newSession: message.session_id,
448
490
  unlockedAttachPin,
449
491
  resumed,
492
+ walletStatusRefreshed,
450
493
  };
451
494
  }
452
495
 
@@ -110,6 +110,7 @@ export const getPassphraseStateWithRefreshDeviceInfo = async (
110
110
  initSession?: boolean;
111
111
  deriveCardano?: boolean;
112
112
  rejectAttachPinForMainWallet?: boolean;
113
+ mainPinSelected?: boolean;
113
114
  }
114
115
  ) => {
115
116
  if (device.isProtocolV2()) {
@@ -127,6 +128,7 @@ export const getPassphraseStateWithRefreshDeviceInfo = async (
127
128
  onlyMainPin: options?.onlyMainPin,
128
129
  deriveCardano: options?.deriveCardano,
129
130
  rejectAttachPinForMainWallet: options?.rejectAttachPinForMainWallet,
131
+ mainPinSelected: options?.mainPinSelected,
130
132
  });
131
133
  }
132
134
 
@@ -194,6 +196,7 @@ export const getPassphraseState = async (
194
196
  initSession?: boolean;
195
197
  deriveCardano?: boolean;
196
198
  rejectAttachPinForMainWallet?: boolean;
199
+ mainPinSelected?: boolean;
197
200
  }
198
201
  ): Promise<{
199
202
  passphraseState: string | undefined;
@@ -216,6 +219,7 @@ export const getPassphraseState = async (
216
219
  onlyMainPin: options?.onlyMainPin,
217
220
  deriveCardano: options?.deriveCardano,
218
221
  rejectAttachPinForMainWallet: options?.rejectAttachPinForMainWallet,
222
+ mainPinSelected: options?.mainPinSelected,
219
223
  });
220
224
  }
221
225