@onekeyfe/hd-core 1.2.0-alpha.42 → 1.2.0-alpha.44

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 (32) hide show
  1. package/__tests__/deviceUploadNft.test.ts +87 -2
  2. package/__tests__/open-wallet-session.test.ts +161 -21
  3. package/__tests__/protocol-v2-ui-lifecycle.test.ts +35 -0
  4. package/__tests__/protocol-v2-unlock-policy.test.ts +110 -0
  5. package/__tests__/protocol-v2.test.ts +71 -13
  6. package/__tests__/protocolV2UiInteraction.test.ts +63 -0
  7. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +1 -0
  8. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/device/Device.d.ts +3 -1
  11. package/dist/device/Device.d.ts.map +1 -1
  12. package/dist/device/DeviceCommands.d.ts +4 -4
  13. package/dist/events/ui-request.d.ts +1 -0
  14. package/dist/events/ui-request.d.ts.map +1 -1
  15. package/dist/index.d.ts +6 -3
  16. package/dist/index.js +174 -45
  17. package/dist/protocols/protocol-v2/uiInteraction.d.ts +4 -1
  18. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  19. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  20. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  21. package/dist/utils/pro2Nft.d.ts +2 -0
  22. package/dist/utils/pro2Nft.d.ts.map +1 -1
  23. package/package.json +4 -4
  24. package/src/api/protocol-v2/DeviceUploadNft.ts +25 -2
  25. package/src/core/index.ts +18 -2
  26. package/src/data/messages/messages-protocol-v2.json +15 -0
  27. package/src/device/Device.ts +15 -5
  28. package/src/events/ui-request.ts +1 -0
  29. package/src/protocols/protocol-v2/uiInteraction.ts +20 -6
  30. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  31. package/src/protocols/protocol-v2/walletSession.ts +84 -24
  32. package/src/utils/pro2Nft.ts +51 -0
@@ -12244,11 +12244,26 @@
12244
12244
  "DeviceSessionError_Busy": 5
12245
12245
  }
12246
12246
  },
12247
+ "DeviceSessionSeedDomain": {
12248
+ "values": {
12249
+ "SeedDomain_Standard": 1,
12250
+ "SeedDomain_Cardano": 2
12251
+ }
12252
+ },
12247
12253
  "DeviceSessionGet": {
12248
12254
  "fields": {
12249
12255
  "session_id": {
12250
12256
  "type": "bytes",
12251
12257
  "id": 1
12258
+ },
12259
+ "btc_test_address": {
12260
+ "type": "string",
12261
+ "id": 2
12262
+ },
12263
+ "seed_domains": {
12264
+ "rule": "repeated",
12265
+ "type": "DeviceSessionSeedDomain",
12266
+ "id": 3
12252
12267
  }
12253
12268
  }
12254
12269
  },
@@ -1644,18 +1644,28 @@ export class Device extends EventEmitter {
1644
1644
  });
1645
1645
  }
1646
1646
 
1647
- finishProtocolV2UiInteraction(outcome: HardwareUiInteractionMeta['outcome'] = 'succeeded') {
1647
+ finishProtocolV2UiInteraction(
1648
+ outcome?: HardwareUiInteractionMeta['outcome'],
1649
+ options?: { ensureMetadata?: boolean }
1650
+ ) {
1648
1651
  const interaction = this.protocolV2UiInteraction;
1649
- if (!interaction?.opened) {
1652
+ if (!interaction || (!interaction.opened && !options?.ensureMetadata)) {
1650
1653
  this.protocolV2UiInteraction = undefined;
1651
1654
  return undefined;
1652
1655
  }
1653
1656
 
1654
1657
  const phaseId = `${interaction.interactionId}:phase-${Math.max(interaction.phaseCounter, 1)}`;
1655
- const metadata = this.createProtocolV2UiPhaseMetadata('processing', 'finish', {
1658
+ interaction.opened = true;
1659
+ interaction.sequence += 1;
1660
+ const metadata: HardwareUiInteractionMeta = {
1661
+ interactionId: interaction.interactionId,
1656
1662
  phaseId,
1657
- outcome,
1658
- });
1663
+ sequence: interaction.sequence,
1664
+ phase: 'processing',
1665
+ transition: 'finish',
1666
+ outcome: outcome ?? 'succeeded',
1667
+ protocol: 'V2',
1668
+ };
1659
1669
  this.protocolV2UiInteraction = undefined;
1660
1670
  return metadata;
1661
1671
  }
@@ -64,6 +64,7 @@ export type HardwareUiInteractionMeta = {
64
64
  transition: 'start' | 'complete' | 'finish';
65
65
  outcome?: 'submitted' | 'succeeded' | 'failed' | 'cancelled' | 'disconnected';
66
66
  protocol: 'V2';
67
+ device?: Device;
67
68
  };
68
69
 
69
70
  export type ProtocolV2UiEventMetadata = {
@@ -203,20 +203,34 @@ export class ProtocolV2UiInteractionCoordinator {
203
203
  this.enterMethodInteraction(this.methodInteraction);
204
204
  }
205
205
 
206
- close() {
206
+ close(
207
+ options: {
208
+ ensureOperationClose?: boolean;
209
+ protocolV2Operation?: boolean;
210
+ } = {}
211
+ ): boolean {
207
212
  if (
208
- !this.device.isProtocolV2() ||
209
- (!this.opened && !this.device.hasOpenProtocolV2UiInteraction?.()) ||
213
+ (!options.protocolV2Operation && !this.device.isProtocolV2()) ||
214
+ (!options.ensureOperationClose &&
215
+ !this.opened &&
216
+ !this.device.hasOpenProtocolV2UiInteraction?.()) ||
210
217
  this.closed
211
218
  )
212
- return;
219
+ return false;
213
220
  this.closed = true;
214
- const interaction = this.device.finishProtocolV2UiInteraction?.();
221
+ const interaction = this.device.finishProtocolV2UiInteraction?.('succeeded', {
222
+ ensureMetadata: options.ensureOperationClose,
223
+ });
224
+ const device = this.device.toMessageObject();
215
225
  this.postMessage(
216
226
  interaction
217
- ? createUiMessage(UI_REQUEST.CLOSE_UI_WINDOW, interaction)
227
+ ? createUiMessage(UI_REQUEST.CLOSE_UI_WINDOW, {
228
+ ...interaction,
229
+ ...(device ? { device } : {}),
230
+ })
218
231
  : createUiMessage(UI_REQUEST.CLOSE_UI_WINDOW)
219
232
  );
233
+ return true;
220
234
  }
221
235
 
222
236
  private emit(
@@ -80,7 +80,14 @@ export async function runMethodWithUnlockPolicy<T = unknown>(
80
80
 
81
81
  await afterStatusBeforeUnlock?.();
82
82
 
83
- if (!status.unlocked) {
83
+ const isStandardWalletRequest =
84
+ method.unlockPolicy !== 'unlock-before-run' &&
85
+ method.useDevicePassphraseState &&
86
+ method.payload?.useEmptyPassphrase === true;
87
+ const standardWalletSessionOwnsUnlock =
88
+ isStandardWalletRequest && status.passphraseProtection === true;
89
+
90
+ if (!status.unlocked && !standardWalletSessionOwnsUnlock) {
84
91
  const unlockInteraction: HardwareUiInteractionMeta | undefined = shouldCoordinateUi
85
92
  ? uiCoordinator.enterUnlockInteraction(method.name)
86
93
  : undefined;
@@ -1,5 +1,5 @@
1
1
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
+ import { DeviceSessionPinType, DeviceSessionSeedDomain } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import { DEVICE } from '../../events';
5
5
  import { assertCompleteDeviceSession } from './deviceSession';
@@ -63,12 +63,36 @@ 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 buildDeviceSessionGetRequest = ({
67
+ sessionId,
68
+ expectedPassphraseState,
69
+ deriveCardano,
70
+ }: {
71
+ sessionId?: string;
72
+ expectedPassphraseState?: string;
73
+ deriveCardano?: boolean;
74
+ } = {}): DeviceSessionGet => ({
75
+ ...(sessionId ? { session_id: sessionId } : {}),
76
+ ...(expectedPassphraseState ? { btc_test_address: expectedPassphraseState } : {}),
77
+ seed_domains:
78
+ deriveCardano === undefined
79
+ ? []
80
+ : [
81
+ DeviceSessionSeedDomain.SeedDomain_Standard,
82
+ ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
83
+ ],
84
+ });
85
+
66
86
  const askDevicePassphrase = async (device: Device, requestPayload: DeviceSessionAskPassphrase) => {
67
87
  await device.commands.typedCall('DeviceSessionAskPassphrase', 'Success', requestPayload);
68
88
  await refreshProtocolV2DeviceStatus(device);
69
89
  };
70
90
 
71
- const selectDeviceSession = async (device: Device, expectedPassphraseState?: string) => {
91
+ const selectDeviceSession = async (
92
+ device: Device,
93
+ expectedPassphraseState?: string,
94
+ deriveCardano?: boolean
95
+ ) => {
72
96
  const existsAttachPinUser = device.features?.attachToPinEnabled === true;
73
97
  const metadata = {
74
98
  source: 'wallet-session-coordinator' as const,
@@ -129,7 +153,7 @@ const selectDeviceSession = async (device: Device, expectedPassphraseState?: str
129
153
  emitUiEvent: false,
130
154
  interaction: attachPinInteraction,
131
155
  });
132
- return getDeviceSession(device, {});
156
+ return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
133
157
  }
134
158
 
135
159
  if (hasHostPassphrase) {
@@ -137,7 +161,7 @@ const selectDeviceSession = async (device: Device, expectedPassphraseState?: str
137
161
  passphrase: hostPassphrase,
138
162
  on_device: false,
139
163
  });
140
- return getDeviceSession(device, {});
164
+ return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
141
165
  }
142
166
 
143
167
  device.emit(DEVICE.PASSPHRASE_ON_DEVICE, device, {
@@ -145,7 +169,7 @@ const selectDeviceSession = async (device: Device, expectedPassphraseState?: str
145
169
  ...(passphraseInteraction ? { interaction: passphraseInteraction } : {}),
146
170
  });
147
171
  await askDevicePassphrase(device, { on_device: true });
148
- return getDeviceSession(device, {});
172
+ return getDeviceSession(device, buildDeviceSessionGetRequest({ deriveCardano }));
149
173
  };
150
174
 
151
175
  export async function getProtocolV2WalletSession(
@@ -191,7 +215,10 @@ export async function getProtocolV2WalletSession(
191
215
  : undefined;
192
216
  let response;
193
217
  let resumed = false;
194
- let mainWalletSelected = false;
218
+ let mainPinSelected =
219
+ options?.onlyMainPin === true &&
220
+ device.features?.unlocked === true &&
221
+ device.features?.unlockedAttachPin === false;
195
222
 
196
223
  const clearCurrentWalletSession = () => {
197
224
  if (options?.onlyMainPin) {
@@ -216,14 +243,25 @@ export async function getProtocolV2WalletSession(
216
243
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
217
244
  };
218
245
 
219
- const selectMainWallet = async (force = false) => {
220
- if (force || !mainWalletSelected) {
246
+ const selectMainPin = async (force = false) => {
247
+ if (force || !mainPinSelected) {
221
248
  await device.unlockDevice(DeviceSessionPinType.Main, {
222
249
  source: 'wallet-session-coordinator',
223
250
  reason: expectedPassphraseState ? 'session-recovery' : 'open-wallet',
224
251
  deviceOnly: true,
225
252
  });
226
- mainWalletSelected = true;
253
+ mainPinSelected = true;
254
+ }
255
+ };
256
+
257
+ const selectStandardWallet = async () => {
258
+ // Main PIN authenticates the device; an empty host passphrase selects the standard derivation.
259
+ await selectMainPin();
260
+ if (device.features?.passphraseProtection === true) {
261
+ await askDevicePassphrase(device, {
262
+ passphrase: '',
263
+ on_device: false,
264
+ });
227
265
  }
228
266
  };
229
267
 
@@ -232,11 +270,16 @@ export async function getProtocolV2WalletSession(
232
270
  if (cachedStandardSession) {
233
271
  try {
234
272
  if (options?.selectMainWalletBeforeRestore) {
235
- await selectMainWallet();
273
+ await selectMainPin();
236
274
  }
237
- response = await getDeviceSession(device, {
238
- session_id: cachedStandardSession.sessionId,
239
- });
275
+ response = await getDeviceSession(
276
+ device,
277
+ buildDeviceSessionGetRequest({
278
+ sessionId: cachedStandardSession.sessionId,
279
+ expectedPassphraseState,
280
+ deriveCardano: options?.deriveCardano,
281
+ })
282
+ );
240
283
  resumed = true;
241
284
  } catch (error) {
242
285
  device.clearStandardInternalState?.();
@@ -248,14 +291,22 @@ export async function getProtocolV2WalletSession(
248
291
  }
249
292
 
250
293
  if (!response) {
251
- await selectMainWallet();
252
- response = await getDeviceSession(device, {});
294
+ await selectStandardWallet();
295
+ response = await getDeviceSession(
296
+ device,
297
+ buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
298
+ );
253
299
  }
254
300
  } else if (cachedSessionId && expectedPassphraseState) {
255
301
  try {
256
- response = await getDeviceSession(device, {
257
- session_id: cachedSessionId,
258
- });
302
+ response = await getDeviceSession(
303
+ device,
304
+ buildDeviceSessionGetRequest({
305
+ sessionId: cachedSessionId,
306
+ expectedPassphraseState,
307
+ deriveCardano: options?.deriveCardano,
308
+ })
309
+ );
259
310
  resumed = true;
260
311
  } catch (error) {
261
312
  device.clearInternalState();
@@ -266,7 +317,13 @@ export async function getProtocolV2WalletSession(
266
317
  }
267
318
  } else if (expectedPassphraseState) {
268
319
  try {
269
- response = await getDeviceSession(device, {});
320
+ response = await getDeviceSession(
321
+ device,
322
+ buildDeviceSessionGetRequest({
323
+ expectedPassphraseState,
324
+ deriveCardano: options?.deriveCardano,
325
+ })
326
+ );
270
327
  } catch (error) {
271
328
  if (options?.resumeOnly || !isWalletSessionInvalidError(error)) {
272
329
  throw error;
@@ -279,7 +336,7 @@ export async function getProtocolV2WalletSession(
279
336
  device.clearInternalState();
280
337
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
281
338
  }
282
- response = await selectDeviceSession(device, expectedPassphraseState);
339
+ response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
283
340
  }
284
341
 
285
342
  let { message } = response;
@@ -302,11 +359,14 @@ export async function getProtocolV2WalletSession(
302
359
  }
303
360
  if (options?.onlyMainPin) {
304
361
  device.clearStandardInternalState?.();
305
- await selectMainWallet(true);
306
- response = await getDeviceSession(device, {});
362
+ await selectStandardWallet();
363
+ response = await getDeviceSession(
364
+ device,
365
+ buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
366
+ );
307
367
  } else {
308
368
  device.clearInternalState();
309
- response = await selectDeviceSession(device, expectedPassphraseState);
369
+ response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
310
370
  }
311
371
  message = response.message;
312
372
  try {
@@ -342,7 +402,7 @@ export async function getProtocolV2WalletSession(
342
402
  return {
343
403
  passphraseState: message.btc_test_address,
344
404
  newSession: message.session_id,
345
- unlockedAttachPin: mainWalletSelected ? false : undefined,
405
+ unlockedAttachPin: mainPinSelected ? false : undefined,
346
406
  resumed,
347
407
  };
348
408
  }
@@ -14,6 +14,7 @@ export const PRO2_NFT_DEFAULT_PACE_MS = 20;
14
14
  export const PRO2_NFT_DEFAULT_TIMEOUT_MS = 15_000;
15
15
  export const PRO2_NFT_MIN_CHUNK_SIZE = 64;
16
16
  export const PRO2_NFT_MAX_CHUNK_SIZE = 2048;
17
+ export const PRO2_NFT_MAX_ITEMS = 10;
17
18
 
18
19
  export type Pro2NftImage = {
19
20
  width: number;
@@ -28,6 +29,56 @@ export type Pro2NftBundle = {
28
29
  metadata: Uint8Array;
29
30
  };
30
31
 
32
+ const PRO2_NFT_BASENAME_PATTERN = /^nft-[0-9a-f]{8}-[1-9][0-9]*$/;
33
+
34
+ type Pro2NftFileType = 'image' | 'thumbnail' | 'metadata';
35
+
36
+ function parsePro2NftFile(listedPath: string):
37
+ | {
38
+ basename: string;
39
+ fileType: Pro2NftFileType;
40
+ }
41
+ | undefined {
42
+ const filename = listedPath.trim().split('/').at(-1);
43
+ if (!filename) return undefined;
44
+
45
+ let basename: string | undefined;
46
+ let fileType: Pro2NftFileType | undefined;
47
+ if (filename.endsWith('_m.bin')) {
48
+ basename = filename.slice(0, -'_m.bin'.length);
49
+ fileType = 'thumbnail';
50
+ } else if (filename.endsWith('.json')) {
51
+ basename = filename.slice(0, -'.json'.length);
52
+ fileType = 'metadata';
53
+ } else if (filename.endsWith('.bin')) {
54
+ basename = filename.slice(0, -'.bin'.length);
55
+ fileType = 'image';
56
+ }
57
+
58
+ return basename && fileType && PRO2_NFT_BASENAME_PATTERN.test(basename)
59
+ ? { basename, fileType }
60
+ : undefined;
61
+ }
62
+
63
+ export function getCompletePro2NftBasenames(childFiles?: string): Set<string> {
64
+ const filesByBasename = new Map<string, Set<Pro2NftFileType>>();
65
+
66
+ for (const listedPath of childFiles?.split('\n') ?? []) {
67
+ const file = parsePro2NftFile(listedPath);
68
+ if (file) {
69
+ const fileTypes = filesByBasename.get(file.basename) ?? new Set<Pro2NftFileType>();
70
+ fileTypes.add(file.fileType);
71
+ filesByBasename.set(file.basename, fileTypes);
72
+ }
73
+ }
74
+
75
+ return new Set(
76
+ [...filesByBasename.entries()]
77
+ .filter(([, fileTypes]) => fileTypes.size === 3)
78
+ .map(([basename]) => basename)
79
+ );
80
+ }
81
+
31
82
  function utf8Length(value: string): number {
32
83
  return new TextEncoder().encode(value).byteLength;
33
84
  }