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

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 (100) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +78 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/deviceUploadNft.test.ts +187 -0
  4. package/__tests__/get-device-state.test.ts +104 -12
  5. package/__tests__/homescreen.test.ts +41 -1
  6. package/__tests__/logBlockEvent.test.ts +1 -1
  7. package/__tests__/open-wallet-session.test.ts +259 -18
  8. package/__tests__/pro2Nft.test.ts +108 -0
  9. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  10. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  11. package/__tests__/protocol-v2.test.ts +377 -402
  12. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  13. package/dist/api/BaseMethod.d.ts +3 -1
  14. package/dist/api/BaseMethod.d.ts.map +1 -1
  15. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  16. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  17. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  18. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  19. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  20. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  21. package/dist/api/index.d.ts +1 -0
  22. package/dist/api/index.d.ts.map +1 -1
  23. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  24. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  26. package/dist/core/deviceEventRegistration.d.ts +2 -0
  27. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/device/Device.d.ts +22 -4
  30. package/dist/device/Device.d.ts.map +1 -1
  31. package/dist/events/device.d.ts +4 -0
  32. package/dist/events/device.d.ts.map +1 -1
  33. package/dist/events/logBlockEvent.d.ts.map +1 -1
  34. package/dist/events/ui-request.d.ts +27 -2
  35. package/dist/events/ui-request.d.ts.map +1 -1
  36. package/dist/index.d.ts +88 -7
  37. package/dist/index.js +937 -604
  38. package/dist/inject.d.ts.map +1 -1
  39. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  40. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  41. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  42. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  43. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/api/index.d.ts +2 -1
  49. package/dist/types/api/index.d.ts.map +1 -1
  50. package/dist/types/api/protocolV2.d.ts +3 -0
  51. package/dist/types/api/protocolV2.d.ts.map +1 -1
  52. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  53. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  54. package/dist/utils/homescreen.d.ts +4 -0
  55. package/dist/utils/homescreen.d.ts.map +1 -1
  56. package/dist/utils/index.d.ts +1 -1
  57. package/dist/utils/index.d.ts.map +1 -1
  58. package/dist/utils/patch.d.ts +1 -1
  59. package/dist/utils/patch.d.ts.map +1 -1
  60. package/dist/utils/pro2Nft.d.ts +29 -0
  61. package/dist/utils/pro2Nft.d.ts.map +1 -0
  62. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  63. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  64. package/package.json +4 -4
  65. package/src/api/BaseMethod.ts +8 -10
  66. package/src/api/FirmwareUpdateV4.ts +13 -6
  67. package/src/api/GetPassphraseState.ts +7 -1
  68. package/src/api/OpenWalletSession.ts +54 -17
  69. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  70. package/src/api/device/DeviceUnlock.ts +1 -1
  71. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  72. package/src/api/index.ts +1 -0
  73. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  74. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  75. package/src/core/deviceEventRegistration.ts +4 -0
  76. package/src/core/index.ts +111 -74
  77. package/src/data/messages/messages-protocol-v2.json +10 -0
  78. package/src/device/Device.ts +221 -24
  79. package/src/events/device.ts +4 -0
  80. package/src/events/logBlockEvent.ts +1 -0
  81. package/src/events/ui-request.ts +32 -2
  82. package/src/inject.ts +1 -0
  83. package/src/protocols/protocol-v2/features.ts +9 -2
  84. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  85. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  86. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  87. package/src/protocols/protocol-v2/walletSession.ts +93 -18
  88. package/src/types/api/index.ts +2 -0
  89. package/src/types/api/protocolV2.ts +13 -0
  90. package/src/utils/deviceFeaturesUtils.ts +4 -0
  91. package/src/utils/homescreen.ts +32 -6
  92. package/src/utils/index.ts +6 -1
  93. package/src/utils/pro2Nft.ts +91 -0
  94. package/src/utils/pro2Wallpaper.ts +35 -8
  95. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  96. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  97. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  98. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  99. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  100. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
@@ -56,6 +56,16 @@ export type ProtocolV2UiEventSource =
56
56
 
57
57
  export type ProtocolV2UiCompletion = 'page-accepted' | 'operation-completed';
58
58
 
59
+ export type HardwareUiInteractionMeta = {
60
+ interactionId: string;
61
+ phaseId: string;
62
+ sequence: number;
63
+ phase: 'pin' | 'passphrase' | 'passphrase-on-device' | 'button' | 'processing';
64
+ transition: 'start' | 'complete' | 'finish';
65
+ outcome?: 'submitted' | 'succeeded' | 'failed' | 'cancelled' | 'disconnected';
66
+ protocol: 'V2';
67
+ };
68
+
59
69
  export type ProtocolV2UiEventMetadata = {
60
70
  source?: ProtocolV2UiEventSource;
61
71
  reason?: string;
@@ -64,12 +74,29 @@ export type ProtocolV2UiEventMetadata = {
64
74
  method?: string;
65
75
  page?: string | number;
66
76
  operation?: string;
77
+ interaction?: HardwareUiInteractionMeta;
67
78
  };
68
79
 
80
+ export type UiRequestWindowClose =
81
+ | {
82
+ type: typeof UI_REQUEST.CLOSE_UI_WINDOW;
83
+ payload: HardwareUiInteractionMeta;
84
+ }
85
+ | {
86
+ type: typeof UI_REQUEST.CLOSE_UI_PIN_WINDOW;
87
+ payload: HardwareUiInteractionMeta;
88
+ }
89
+ | {
90
+ type: typeof UI_REQUEST.CLOSE_UI_WINDOW;
91
+ payload?: undefined;
92
+ }
93
+ | {
94
+ type: typeof UI_REQUEST.CLOSE_UI_PIN_WINDOW;
95
+ payload?: undefined;
96
+ };
97
+
69
98
  export interface UiRequestWithoutPayload {
70
99
  type:
71
- | typeof UI_REQUEST.CLOSE_UI_WINDOW
72
- | typeof UI_REQUEST.CLOSE_UI_PIN_WINDOW
73
100
  | typeof UI_REQUEST.BLUETOOTH_PERMISSION
74
101
  | typeof UI_REQUEST.BLUETOOTH_UNSUPPORTED
75
102
  | typeof UI_REQUEST.BLUETOOTH_POWERED_OFF
@@ -111,6 +138,7 @@ export interface UiRequestPassphrase {
111
138
  source?: 'wallet-session-coordinator';
112
139
  reason?: 'open-wallet' | 'session-recovery';
113
140
  expectedPassphraseState?: string;
141
+ interaction?: HardwareUiInteractionMeta;
114
142
  };
115
143
  }
116
144
 
@@ -121,6 +149,7 @@ export interface UiRequestPassphraseOnDevice {
121
149
  passphraseState?: string;
122
150
  source?: 'wallet-session-coordinator';
123
151
  reason?: 'open-wallet' | 'session-recovery';
152
+ interaction?: HardwareUiInteractionMeta;
124
153
  };
125
154
  }
126
155
 
@@ -191,6 +220,7 @@ export interface PreviousAddressResult {
191
220
 
192
221
  export type UiEvent =
193
222
  | UiRequestWithoutPayload
223
+ | UiRequestWindowClose
194
224
  | UiRequestDeviceAction
195
225
  | UiRequestButton
196
226
  | UiRequestPassphraseOnDevice
package/src/inject.ts CHANGED
@@ -162,6 +162,7 @@ export const createCoreApi = (
162
162
  call({ ...params, connectId, method: 'deviceGetOnboardingStatus' }),
163
163
  deviceUploadWallpaper: (connectId, params) =>
164
164
  call({ ...params, connectId, method: 'deviceUploadWallpaper' }),
165
+ deviceUploadNft: (connectId, params) => call({ ...params, connectId, method: 'deviceUploadNft' }),
165
166
  uploadPortfolio: (connectId, params) => call({ ...params, connectId, method: 'uploadPortfolio' }),
166
167
  deviceRecovery: (connectId, params) => call({ ...params, connectId, method: 'deviceRecovery' }),
167
168
  deviceReset: (connectId, params) => call({ ...params, connectId, method: 'deviceReset' }),
@@ -168,8 +168,15 @@ export async function requestProtocolV2ProtocolInfo({
168
168
  }): Promise<ProtocolInfo> {
169
169
  const response =
170
170
  timeoutMs === undefined
171
- ? await commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {})
172
- : await commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {}, { timeoutMs });
171
+ ? await commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
172
+ eventless_wallet_session: true,
173
+ })
174
+ : await commands.typedCall(
175
+ 'ProtocolInfoRequest',
176
+ 'ProtocolInfo',
177
+ { eventless_wallet_session: true },
178
+ { timeoutMs }
179
+ );
173
180
  return response.message;
174
181
  }
175
182
 
@@ -1,6 +1,6 @@
1
1
  import { UI_REQUEST, createUiMessage } from '../../events';
2
2
 
3
- import type { CoreMessage, ProtocolV2UiCompletion } from '../../events';
3
+ import type { CoreMessage, HardwareUiInteractionMeta, ProtocolV2UiCompletion } from '../../events';
4
4
  import type { Device } from '../../device/Device';
5
5
  import type { KnownDevice } from '../../types';
6
6
 
@@ -28,7 +28,15 @@ type ProtocolV2InteractionMethod = {
28
28
  protocolV2UiInteraction?: ProtocolV2InteractionDescriptor;
29
29
  };
30
30
 
31
- type InteractionDevice = Pick<Device, 'isProtocolV2' | 'toMessageObject'>;
31
+ type InteractionDevice = Pick<Device, 'isProtocolV2' | 'toMessageObject'> &
32
+ Partial<
33
+ Pick<
34
+ Device,
35
+ | 'createProtocolV2UiPhaseMetadata'
36
+ | 'finishProtocolV2UiInteraction'
37
+ | 'hasOpenProtocolV2UiInteraction'
38
+ >
39
+ >;
32
40
  type PostMessage = (message: CoreMessage) => void;
33
41
 
34
42
  export const isProtocolV2UiEnabled = (method: { protocolV2UiMode?: 'auto' | 'none' }) =>
@@ -156,12 +164,17 @@ export class ProtocolV2UiInteractionCoordinator {
156
164
  this.methodInteraction = interaction;
157
165
  if (!interaction) return;
158
166
  const { request, ...metadata } = interaction;
167
+ const interactionMeta = this.device.createProtocolV2UiPhaseMetadata?.(
168
+ request === 'pin' ? 'pin' : 'button',
169
+ 'start'
170
+ );
159
171
 
160
172
  this.emit(
161
173
  request === 'pin' ? UI_REQUEST.REQUEST_PIN : UI_REQUEST.REQUEST_BUTTON,
162
174
  {
163
175
  device: this.device.toMessageObject() as KnownDevice,
164
176
  ...metadata,
177
+ ...(interactionMeta ? { interaction: interactionMeta } : {}),
165
178
  },
166
179
  `method:${request}:${interaction.reason}:${interaction.page ?? ''}:${
167
180
  interaction.operation ?? ''
@@ -169,7 +182,8 @@ export class ProtocolV2UiInteractionCoordinator {
169
182
  );
170
183
  }
171
184
 
172
- enterUnlockInteraction(method?: string) {
185
+ enterUnlockInteraction(method?: string): HardwareUiInteractionMeta | undefined {
186
+ const interaction = this.device.createProtocolV2UiPhaseMetadata?.('pin', 'start');
173
187
  this.emit(
174
188
  UI_REQUEST.REQUEST_PIN,
175
189
  {
@@ -178,9 +192,11 @@ export class ProtocolV2UiInteractionCoordinator {
178
192
  reason: 'device-locked',
179
193
  deviceOnly: true,
180
194
  method,
195
+ ...(interaction ? { interaction } : {}),
181
196
  },
182
197
  `unlock:${method ?? ''}`
183
198
  );
199
+ return interaction;
184
200
  }
185
201
 
186
202
  resumeMethodInteraction() {
@@ -188,9 +204,19 @@ export class ProtocolV2UiInteractionCoordinator {
188
204
  }
189
205
 
190
206
  close() {
191
- if (!this.device.isProtocolV2() || !this.opened || this.closed) return;
207
+ if (
208
+ !this.device.isProtocolV2() ||
209
+ (!this.opened && !this.device.hasOpenProtocolV2UiInteraction?.()) ||
210
+ this.closed
211
+ )
212
+ return;
192
213
  this.closed = true;
193
- this.postMessage(createUiMessage(UI_REQUEST.CLOSE_UI_WINDOW));
214
+ const interaction = this.device.finishProtocolV2UiInteraction?.();
215
+ this.postMessage(
216
+ interaction
217
+ ? createUiMessage(UI_REQUEST.CLOSE_UI_WINDOW, interaction)
218
+ : createUiMessage(UI_REQUEST.CLOSE_UI_WINDOW)
219
+ );
194
220
  }
195
221
 
196
222
  private emit(
@@ -1,105 +1 @@
1
- export type UnlockPolicy = 'none' | 'unlock-before-run' | 'retry-on-locked';
2
-
3
- /**
4
- * Protocol V2 methods that are safe to replay after a locked response.
5
- *
6
- * Keep this as an exact allowlist. Name-pattern matching can silently make a new
7
- * state-changing method replayable when it happens to share a suffix.
8
- */
9
- export const PROTOCOL_V2_RETRY_ON_LOCKED_METHODS = [
10
- 'cipherKeyValue',
11
- 'allNetworkGetAddress',
12
- 'allNetworkGetAddressByLoop',
13
- 'btcGetAddress',
14
- 'btcGetPublicKey',
15
- 'btcSignMessage',
16
- 'btcSignPsbt',
17
- 'btcSignTransaction',
18
- 'btcVerifyMessage',
19
- 'confluxGetAddress',
20
- 'confluxSignMessage',
21
- 'confluxSignTransaction',
22
- 'evmGetAddress',
23
- 'evmGetPublicKey',
24
- 'evmSignMessage',
25
- 'evmSignTransaction',
26
- 'evmSignTypedData',
27
- 'evmVerifyMessage',
28
- 'starcoinGetAddress',
29
- 'starcoinGetPublicKey',
30
- 'starcoinSignMessage',
31
- 'starcoinSignTransaction',
32
- 'starcoinVerifyMessage',
33
- 'nemGetAddress',
34
- 'nemSignTransaction',
35
- 'solGetAddress',
36
- 'solSignTransaction',
37
- 'solSignOffchainMessage',
38
- 'solSignMessage',
39
- 'stellarGetAddress',
40
- 'stellarSignTransaction',
41
- 'tronGetAddress',
42
- 'tronSignMessage',
43
- 'tronSignTransaction',
44
- 'nearGetAddress',
45
- 'nearSignTransaction',
46
- 'aptosGetAddress',
47
- 'aptosGetPublicKey',
48
- 'aptosSignTransaction',
49
- 'aptosSignMessage',
50
- 'aptosSignInMessage',
51
- 'algoGetAddress',
52
- 'algoSignTransaction',
53
- 'cosmosGetAddress',
54
- 'cosmosGetPublicKey',
55
- 'cosmosSignTransaction',
56
- 'xrpGetAddress',
57
- 'xrpSignTransaction',
58
- 'suiGetAddress',
59
- 'suiGetPublicKey',
60
- 'suiSignMessage',
61
- 'suiSignTransaction',
62
- 'cardanoGetAddress',
63
- 'cardanoGetPublicKey',
64
- 'cardanoSignTransaction',
65
- 'cardanoSignMessage',
66
- 'filecoinGetAddress',
67
- 'filecoinSignTransaction',
68
- 'polkadotGetAddress',
69
- 'polkadotSignTransaction',
70
- 'kaspaGetAddress',
71
- 'kaspaSignTransaction',
72
- 'nexaGetAddress',
73
- 'nexaSignTransaction',
74
- 'nostrGetPublicKey',
75
- 'nostrSignEvent',
76
- 'nostrEncryptMessage',
77
- 'nostrDecryptMessage',
78
- 'nostrSignSchnorr',
79
- 'lnurlAuth',
80
- 'nervosGetAddress',
81
- 'nervosSignTransaction',
82
- 'dnxGetAddress',
83
- 'dnxSignTransaction',
84
- 'tonGetAddress',
85
- 'tonSignMessage',
86
- 'tonSignProof',
87
- 'tonSignData',
88
- 'scdoGetAddress',
89
- 'scdoSignTransaction',
90
- 'scdoSignMessage',
91
- 'alephiumGetAddress',
92
- 'alephiumSignTransaction',
93
- 'alephiumSignMessage',
94
- 'benfenGetAddress',
95
- 'benfenGetPublicKey',
96
- 'benfenSignMessage',
97
- 'benfenSignTransaction',
98
- 'neoGetAddress',
99
- 'neoSignTransaction',
100
- ] as const;
101
-
102
- const retryOnLockedMethods = new Set<string>(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS);
103
-
104
- export const getProtocolV2UnlockPolicy = (methodName: string): UnlockPolicy =>
105
- retryOnLockedMethods.has(methodName) ? 'retry-on-locked' : 'none';
1
+ export type UnlockPolicy = 'none' | 'unlock-before-run';
@@ -0,0 +1,117 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import { LoggerNames, getLogger } from '../../utils';
4
+ import { isProtocolV2UiEnabled, resolveProtocolV2UiInteraction } from './uiInteraction';
5
+ import { refreshProtocolV2DeviceStatus } from './walletSession';
6
+
7
+ import type { BaseMethod } from '../../api/BaseMethod';
8
+ import type { Device } from '../../device/Device';
9
+ import type { HardwareUiInteractionMeta } from '../../events';
10
+ import type { ProtocolV2UiInteractionCoordinator } from './uiInteraction';
11
+
12
+ const Log = getLogger(LoggerNames.Core);
13
+
14
+ export type ProtocolV2UnlockContext = {
15
+ preflightCompleted: boolean;
16
+ };
17
+
18
+ export const createProtocolV2UnlockContext = (): ProtocolV2UnlockContext => ({
19
+ preflightCompleted: false,
20
+ });
21
+
22
+ type RunnableMethod = Pick<
23
+ BaseMethod,
24
+ | 'run'
25
+ | 'unlockPolicy'
26
+ | 'useDevicePassphraseState'
27
+ | 'protocolV2UiInteraction'
28
+ | 'protocolV2UiMode'
29
+ | 'params'
30
+ | 'payload'
31
+ > & {
32
+ name?: string;
33
+ protocolV2UnlockContext?: ProtocolV2UnlockContext;
34
+ };
35
+
36
+ type UiInteractionCoordinator = Pick<
37
+ ProtocolV2UiInteractionCoordinator,
38
+ 'enterMethodInteraction' | 'enterUnlockInteraction'
39
+ >;
40
+
41
+ type RunMethodWithUnlockPolicyOptions<T> = {
42
+ context?: ProtocolV2UnlockContext;
43
+ uiCoordinator?: UiInteractionCoordinator;
44
+ afterStatusBeforeUnlock?: () => void | Promise<void>;
45
+ prepare?: () => Promise<void>;
46
+ run?: () => Promise<T>;
47
+ };
48
+
49
+ export async function runMethodWithUnlockPolicy<T = unknown>(
50
+ method: RunnableMethod,
51
+ device: Device,
52
+ options: RunMethodWithUnlockPolicyOptions<T> = {}
53
+ ): Promise<T> {
54
+ const {
55
+ context = createProtocolV2UnlockContext(),
56
+ uiCoordinator,
57
+ afterStatusBeforeUnlock,
58
+ prepare,
59
+ run: configuredRun,
60
+ } = options;
61
+ const run = configuredRun ?? (() => method.run() as Promise<T>);
62
+ method.protocolV2UnlockContext = context;
63
+
64
+ const shouldEmitUi = isProtocolV2UiEnabled(method);
65
+ const shouldCoordinateUi = shouldEmitUi && uiCoordinator !== undefined;
66
+ const requiresPreUnlock =
67
+ device.isProtocolV2() &&
68
+ (method.useDevicePassphraseState || method.unlockPolicy === 'unlock-before-run') &&
69
+ !device.isBootloader?.() &&
70
+ !device.isRomloader?.();
71
+
72
+ if (requiresPreUnlock && !context.preflightCompleted) {
73
+ const status = await refreshProtocolV2DeviceStatus(device);
74
+ if (typeof status?.unlocked !== 'boolean') {
75
+ throw ERRORS.TypedError(
76
+ HardwareErrorCode.RuntimeError,
77
+ 'Protocol V2 DeviceStatus did not report an explicit unlock state.'
78
+ );
79
+ }
80
+
81
+ await afterStatusBeforeUnlock?.();
82
+
83
+ if (!status.unlocked) {
84
+ const unlockInteraction: HardwareUiInteractionMeta | undefined = shouldCoordinateUi
85
+ ? uiCoordinator.enterUnlockInteraction(method.name)
86
+ : undefined;
87
+ const unlockedStatus = await device.unlockDevice(
88
+ undefined,
89
+ shouldCoordinateUi
90
+ ? { emitUiEvent: false, interaction: unlockInteraction }
91
+ : {
92
+ source: 'unlock-coordinator',
93
+ reason: 'device-locked',
94
+ deviceOnly: true,
95
+ method: method.name,
96
+ }
97
+ );
98
+ if (unlockedStatus?.unlocked !== true) {
99
+ throw ERRORS.TypedError(
100
+ HardwareErrorCode.RuntimeError,
101
+ 'Protocol V2 device remained locked after the unlock flow.'
102
+ );
103
+ }
104
+ Log.debug('Protocol V2 pre-unlock completed', { method: method.name });
105
+ }
106
+
107
+ context.preflightCompleted = true;
108
+ }
109
+
110
+ await prepare?.();
111
+
112
+ if (shouldCoordinateUi) {
113
+ uiCoordinator.enterMethodInteraction(resolveProtocolV2UiInteraction(method));
114
+ }
115
+
116
+ return run();
117
+ }
@@ -38,10 +38,26 @@ export async function refreshProtocolV2DeviceStatus(device: Device) {
38
38
  return device.updateProtocolV2Status(status);
39
39
  }
40
40
 
41
- const negotiateEventlessWalletSession = async (device: Device) => {
42
- await device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
43
- eventless_wallet_session: true,
41
+ export async function ensureProtocolV2WalletSessionUnlocked(device: Device) {
42
+ if (!device.isProtocolV2() || device.isBootloader?.() || device.isRomloader?.()) {
43
+ return false;
44
+ }
45
+
46
+ await refreshProtocolV2DeviceStatus(device);
47
+ if (device.state?.status.unlocked !== false) {
48
+ return false;
49
+ }
50
+
51
+ await device.unlockDevice(DeviceSessionPinType.Main, {
52
+ source: 'unlock-coordinator',
53
+ reason: 'device-locked',
54
+ deviceOnly: true,
44
55
  });
56
+ return true;
57
+ }
58
+
59
+ const negotiateEventlessWalletSession = async (device: Device) => {
60
+ await device.ensureProtocolV2RuntimeContext();
45
61
  };
46
62
 
47
63
  const getDeviceSession = async (device: Device, request: DeviceSessionGet) =>
@@ -59,11 +75,13 @@ const selectDeviceSession = async (device: Device, expectedPassphraseState?: str
59
75
  reason: expectedPassphraseState ? ('session-recovery' as const) : ('open-wallet' as const),
60
76
  ...(expectedPassphraseState ? { expectedPassphraseState } : {}),
61
77
  };
78
+ const passphraseInteraction = device.createProtocolV2UiPhaseMetadata?.('passphrase', 'start');
62
79
  const response = await device.commands.promptPassphrase(
63
80
  {
64
81
  existsAttachPinUser,
65
82
  deviceOnly: false,
66
83
  ...metadata,
84
+ ...(passphraseInteraction ? { interaction: passphraseInteraction } : {}),
67
85
  },
68
86
  { cancelDeviceOnReject: false }
69
87
  );
@@ -102,8 +120,15 @@ const selectDeviceSession = async (device: Device, expectedPassphraseState?: str
102
120
  'Attach PIN wallet selection is unavailable on this device.'
103
121
  );
104
122
  }
105
- device.emit(DEVICE.ATTACH_PIN_ON_DEVICE, device, metadata);
106
- await device.unlockDevice(DeviceSessionPinType.AttachToPin);
123
+ const attachPinInteraction = device.createProtocolV2UiPhaseMetadata?.('pin', 'start');
124
+ device.emit(DEVICE.ATTACH_PIN_ON_DEVICE, device, {
125
+ ...metadata,
126
+ ...(attachPinInteraction ? { interaction: attachPinInteraction } : {}),
127
+ });
128
+ await device.unlockDevice(DeviceSessionPinType.AttachToPin, {
129
+ emitUiEvent: false,
130
+ interaction: attachPinInteraction,
131
+ });
107
132
  return getDeviceSession(device, {});
108
133
  }
109
134
 
@@ -115,7 +140,10 @@ const selectDeviceSession = async (device: Device, expectedPassphraseState?: str
115
140
  return getDeviceSession(device, {});
116
141
  }
117
142
 
118
- device.emit(DEVICE.PASSPHRASE_ON_DEVICE, device, metadata);
143
+ device.emit(DEVICE.PASSPHRASE_ON_DEVICE, device, {
144
+ ...metadata,
145
+ ...(passphraseInteraction ? { interaction: passphraseInteraction } : {}),
146
+ });
119
147
  await askDevicePassphrase(device, { on_device: true });
120
148
  return getDeviceSession(device, {});
121
149
  };
@@ -128,7 +156,9 @@ export async function getProtocolV2WalletSession(
128
156
  initSession?: boolean;
129
157
  expectedPassphraseState?: string;
130
158
  onlyMainPin?: boolean;
159
+ selectMainWalletBeforeRestore?: boolean;
131
160
  resumeOnly?: boolean;
161
+ deriveCardano?: boolean;
132
162
  }
133
163
  ) {
134
164
  const forceWalletSelection =
@@ -161,13 +191,48 @@ export async function getProtocolV2WalletSession(
161
191
  : undefined;
162
192
  let response;
163
193
  let resumed = false;
194
+ let mainWalletSelected = false;
195
+
196
+ const clearCurrentWalletSession = () => {
197
+ if (options?.onlyMainPin) {
198
+ device.clearStandardInternalState?.();
199
+ } else {
200
+ device.clearInternalState();
201
+ }
202
+ };
203
+
204
+ const rejectMismatchedAttachPinWallet = async () => {
205
+ const features = await refreshProtocolV2DeviceStatus(device);
206
+ if (features.unlockedAttachPin !== true) {
207
+ return;
208
+ }
209
+
210
+ try {
211
+ await device.lockDevice();
212
+ } catch {
213
+ // Reject the mismatched Attach PIN wallet even when older firmware cannot lock.
214
+ }
215
+ clearCurrentWalletSession();
216
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckUnlockTypeError);
217
+ };
218
+
219
+ const selectMainWallet = async (force = false) => {
220
+ if (force || !mainWalletSelected) {
221
+ await device.unlockDevice(DeviceSessionPinType.Main, {
222
+ source: 'wallet-session-coordinator',
223
+ reason: expectedPassphraseState ? 'session-recovery' : 'open-wallet',
224
+ deviceOnly: true,
225
+ });
226
+ mainWalletSelected = true;
227
+ }
228
+ };
164
229
 
165
230
  if (options?.onlyMainPin) {
166
231
  expectedPassphraseState = cachedStandardSession?.passphraseState;
167
232
  if (cachedStandardSession) {
168
233
  try {
169
- if (device.features?.unlockedAttachPin === true) {
170
- await device.unlockDevice(DeviceSessionPinType.Main);
234
+ if (options?.selectMainWalletBeforeRestore) {
235
+ await selectMainWallet();
171
236
  }
172
237
  response = await getDeviceSession(device, {
173
238
  session_id: cachedStandardSession.sessionId,
@@ -183,12 +248,14 @@ export async function getProtocolV2WalletSession(
183
248
  }
184
249
 
185
250
  if (!response) {
186
- await device.unlockDevice(DeviceSessionPinType.Main);
251
+ await selectMainWallet();
187
252
  response = await getDeviceSession(device, {});
188
253
  }
189
254
  } else if (cachedSessionId && expectedPassphraseState) {
190
255
  try {
191
- response = await getDeviceSession(device, { session_id: cachedSessionId });
256
+ response = await getDeviceSession(device, {
257
+ session_id: cachedSessionId,
258
+ });
192
259
  resumed = true;
193
260
  } catch (error) {
194
261
  device.clearInternalState();
@@ -197,6 +264,14 @@ export async function getProtocolV2WalletSession(
197
264
  }
198
265
  resumed = false;
199
266
  }
267
+ } else if (expectedPassphraseState) {
268
+ try {
269
+ response = await getDeviceSession(device, {});
270
+ } catch (error) {
271
+ if (options?.resumeOnly || !isWalletSessionInvalidError(error)) {
272
+ throw error;
273
+ }
274
+ }
200
275
  }
201
276
 
202
277
  if (!response) {
@@ -220,13 +295,14 @@ export async function getProtocolV2WalletSession(
220
295
  }
221
296
  if (expectedPassphraseState && expectedPassphraseState !== message.btc_test_address) {
222
297
  resumed = false;
298
+ await rejectMismatchedAttachPinWallet();
223
299
  if (options?.resumeOnly) {
224
300
  device.clearInternalState();
225
301
  throw ERRORS.TypedError(HardwareErrorCode.WalletSessionInvalid);
226
302
  }
227
303
  if (options?.onlyMainPin) {
228
304
  device.clearStandardInternalState?.();
229
- await device.unlockDevice(DeviceSessionPinType.Main);
305
+ await selectMainWallet(true);
230
306
  response = await getDeviceSession(device, {});
231
307
  } else {
232
308
  device.clearInternalState();
@@ -244,11 +320,8 @@ export async function getProtocolV2WalletSession(
244
320
  throw error;
245
321
  }
246
322
  if (expectedPassphraseState !== message.btc_test_address) {
247
- if (options?.onlyMainPin) {
248
- device.clearStandardInternalState?.();
249
- } else {
250
- device.clearInternalState();
251
- }
323
+ await rejectMismatchedAttachPinWallet();
324
+ clearCurrentWalletSession();
252
325
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
253
326
  }
254
327
  }
@@ -269,17 +342,19 @@ export async function getProtocolV2WalletSession(
269
342
  return {
270
343
  passphraseState: message.btc_test_address,
271
344
  newSession: message.session_id,
272
- unlockedAttachPin: device.features?.unlockedAttachPin ?? undefined,
345
+ unlockedAttachPin: mainWalletSelected ? false : undefined,
273
346
  resumed,
274
347
  };
275
348
  }
276
349
 
277
350
  export async function restoreProtocolV2WalletSession(
278
351
  device: Device,
279
- expectedPassphraseState: string
352
+ expectedPassphraseState: string,
353
+ deriveCardano?: boolean
280
354
  ) {
281
355
  return getProtocolV2WalletSession(device, {
282
356
  expectedPassphraseState,
283
357
  resumeOnly: true,
358
+ deriveCardano,
284
359
  });
285
360
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  deviceGetOnboardingStatus,
3
3
  deviceReboot,
4
+ deviceUploadNft,
4
5
  deviceUploadWallpaper,
5
6
  testProtocolV2Ping,
6
7
  uploadPortfolio,
@@ -234,6 +235,7 @@ export type CoreApi = {
234
235
  */
235
236
  deviceReboot: typeof deviceReboot;
236
237
  deviceGetOnboardingStatus: typeof deviceGetOnboardingStatus;
238
+ deviceUploadNft: typeof deviceUploadNft;
237
239
  deviceUploadWallpaper: typeof deviceUploadWallpaper;
238
240
  uploadPortfolio: typeof uploadPortfolio;
239
241
 
@@ -5,6 +5,10 @@ import type {
5
5
  DeviceUploadWallpaperParams,
6
6
  DeviceUploadWallpaperResponse,
7
7
  } from '../../api/protocol-v2/DeviceUploadWallpaper';
8
+ import type {
9
+ DeviceUploadNftParams,
10
+ DeviceUploadNftResponse,
11
+ } from '../../api/protocol-v2/DeviceUploadNft';
8
12
 
9
13
  // Re-export implementation parameter types as the single source of truth.
10
14
  export type { DeviceRebootParams, RebootTypeInput } from '../../api/protocol-v2/helpers';
@@ -12,6 +16,10 @@ export type {
12
16
  DeviceUploadWallpaperParams,
13
17
  DeviceUploadWallpaperResponse,
14
18
  } from '../../api/protocol-v2/DeviceUploadWallpaper';
19
+ export type {
20
+ DeviceUploadNftParams,
21
+ DeviceUploadNftResponse,
22
+ } from '../../api/protocol-v2/DeviceUploadNft';
15
23
 
16
24
  // ── Shared response shapes (Protocol V2 file system) ────────────────────
17
25
 
@@ -52,6 +60,11 @@ export declare function deviceUploadWallpaper(
52
60
  params: CommonParams & DeviceUploadWallpaperParams
53
61
  ): Response<DeviceUploadWallpaperResponse>;
54
62
 
63
+ export declare function deviceUploadNft(
64
+ connectId: string,
65
+ params: CommonParams & DeviceUploadNftParams
66
+ ): Response<DeviceUploadNftResponse>;
67
+
55
68
  export declare function uploadPortfolio(
56
69
  connectId: string,
57
70
  params: {