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

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 +114 -20
  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 +54 -14
  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 +157 -38
  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 +66 -17
  32. package/src/utils/pro2Nft.ts +51 -0
@@ -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 mainWalletSelected =
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) {
@@ -234,9 +261,14 @@ export async function getProtocolV2WalletSession(
234
261
  if (options?.selectMainWalletBeforeRestore) {
235
262
  await selectMainWallet();
236
263
  }
237
- response = await getDeviceSession(device, {
238
- session_id: cachedStandardSession.sessionId,
239
- });
264
+ response = await getDeviceSession(
265
+ device,
266
+ buildDeviceSessionGetRequest({
267
+ sessionId: cachedStandardSession.sessionId,
268
+ expectedPassphraseState,
269
+ deriveCardano: options?.deriveCardano,
270
+ })
271
+ );
240
272
  resumed = true;
241
273
  } catch (error) {
242
274
  device.clearStandardInternalState?.();
@@ -249,13 +281,21 @@ export async function getProtocolV2WalletSession(
249
281
 
250
282
  if (!response) {
251
283
  await selectMainWallet();
252
- response = await getDeviceSession(device, {});
284
+ response = await getDeviceSession(
285
+ device,
286
+ buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
287
+ );
253
288
  }
254
289
  } else if (cachedSessionId && expectedPassphraseState) {
255
290
  try {
256
- response = await getDeviceSession(device, {
257
- session_id: cachedSessionId,
258
- });
291
+ response = await getDeviceSession(
292
+ device,
293
+ buildDeviceSessionGetRequest({
294
+ sessionId: cachedSessionId,
295
+ expectedPassphraseState,
296
+ deriveCardano: options?.deriveCardano,
297
+ })
298
+ );
259
299
  resumed = true;
260
300
  } catch (error) {
261
301
  device.clearInternalState();
@@ -266,7 +306,13 @@ export async function getProtocolV2WalletSession(
266
306
  }
267
307
  } else if (expectedPassphraseState) {
268
308
  try {
269
- response = await getDeviceSession(device, {});
309
+ response = await getDeviceSession(
310
+ device,
311
+ buildDeviceSessionGetRequest({
312
+ expectedPassphraseState,
313
+ deriveCardano: options?.deriveCardano,
314
+ })
315
+ );
270
316
  } catch (error) {
271
317
  if (options?.resumeOnly || !isWalletSessionInvalidError(error)) {
272
318
  throw error;
@@ -279,7 +325,7 @@ export async function getProtocolV2WalletSession(
279
325
  device.clearInternalState();
280
326
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
281
327
  }
282
- response = await selectDeviceSession(device, expectedPassphraseState);
328
+ response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
283
329
  }
284
330
 
285
331
  let { message } = response;
@@ -303,10 +349,13 @@ export async function getProtocolV2WalletSession(
303
349
  if (options?.onlyMainPin) {
304
350
  device.clearStandardInternalState?.();
305
351
  await selectMainWallet(true);
306
- response = await getDeviceSession(device, {});
352
+ response = await getDeviceSession(
353
+ device,
354
+ buildDeviceSessionGetRequest({ deriveCardano: options?.deriveCardano })
355
+ );
307
356
  } else {
308
357
  device.clearInternalState();
309
- response = await selectDeviceSession(device, expectedPassphraseState);
358
+ response = await selectDeviceSession(device, expectedPassphraseState, options?.deriveCardano);
310
359
  }
311
360
  message = response.message;
312
361
  try {
@@ -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
  }