@onekeyfe/hd-core 1.2.0-alpha.41 → 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 (202) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +78 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +14 -1
  4. package/__tests__/connectSettings.test.ts +5 -47
  5. package/__tests__/deviceUploadNft.test.ts +272 -0
  6. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +31 -59
  7. package/__tests__/get-device-state.test.ts +132 -12
  8. package/__tests__/homescreen.test.ts +41 -1
  9. package/__tests__/logBlockEvent.test.ts +1 -1
  10. package/__tests__/open-wallet-session.test.ts +366 -31
  11. package/__tests__/pro2Nft.test.ts +108 -0
  12. package/__tests__/protocol-v2-resources.test.ts +204 -0
  13. package/__tests__/protocol-v2-ui-lifecycle.test.ts +91 -0
  14. package/__tests__/protocol-v2-unlock-policy.test.ts +196 -35
  15. package/__tests__/protocol-v2.test.ts +568 -957
  16. package/__tests__/protocolV2UiInteraction.test.ts +63 -0
  17. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  18. package/__tests__/uiProgressThrottle.test.ts +74 -0
  19. package/dist/api/BaseMethod.d.ts +3 -1
  20. package/dist/api/BaseMethod.d.ts.map +1 -1
  21. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  25. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV4.d.ts +6 -25
  27. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  28. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  29. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  30. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  32. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -8
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/getBinary.d.ts +0 -7
  36. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  37. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  38. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  39. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  40. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  41. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  42. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  43. package/dist/api/index.d.ts +1 -0
  44. package/dist/api/index.d.ts.map +1 -1
  45. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +31 -0
  46. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  47. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  48. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  49. package/dist/core/deviceEventRegistration.d.ts +2 -0
  50. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/data-manager/DataManager.d.ts +3 -2
  53. package/dist/data-manager/DataManager.d.ts.map +1 -1
  54. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  55. package/dist/device/Device.d.ts +24 -4
  56. package/dist/device/Device.d.ts.map +1 -1
  57. package/dist/device/DeviceCommands.d.ts +4 -4
  58. package/dist/events/device.d.ts +4 -0
  59. package/dist/events/device.d.ts.map +1 -1
  60. package/dist/events/logBlockEvent.d.ts.map +1 -1
  61. package/dist/events/ui-request.d.ts +28 -2
  62. package/dist/events/ui-request.d.ts.map +1 -1
  63. package/dist/index.d.ts +115 -184
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2051 -3349
  66. package/dist/inject.d.ts.map +1 -1
  67. package/dist/lowLevelInject.d.ts.map +1 -1
  68. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  69. package/dist/protocols/protocol-v2/resources.d.ts +30 -0
  70. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
  71. package/dist/protocols/protocol-v2/uiInteraction.d.ts +7 -4
  72. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  74. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  75. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  76. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  77. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  78. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  79. package/dist/topLevelInject.d.ts.map +1 -1
  80. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -4
  81. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  82. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  83. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  84. package/dist/types/api/export.d.ts +1 -4
  85. package/dist/types/api/export.d.ts.map +1 -1
  86. package/dist/types/api/firmwareUpdate.d.ts +0 -69
  87. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  88. package/dist/types/api/index.d.ts +3 -10
  89. package/dist/types/api/index.d.ts.map +1 -1
  90. package/dist/types/api/protocolV2.d.ts +3 -0
  91. package/dist/types/api/protocolV2.d.ts.map +1 -1
  92. package/dist/types/settings.d.ts +12 -13
  93. package/dist/types/settings.d.ts.map +1 -1
  94. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  95. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  96. package/dist/utils/homescreen.d.ts +4 -0
  97. package/dist/utils/homescreen.d.ts.map +1 -1
  98. package/dist/utils/index.d.ts +1 -1
  99. package/dist/utils/index.d.ts.map +1 -1
  100. package/dist/utils/patch.d.ts +1 -1
  101. package/dist/utils/patch.d.ts.map +1 -1
  102. package/dist/utils/pro2Nft.d.ts +31 -0
  103. package/dist/utils/pro2Nft.d.ts.map +1 -0
  104. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  105. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  106. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  107. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  108. package/package.json +4 -4
  109. package/src/api/BaseMethod.ts +8 -10
  110. package/src/api/CheckAllFirmwareRelease.ts +40 -53
  111. package/src/api/FirmwareUpdateV2.ts +120 -298
  112. package/src/api/FirmwareUpdateV3.ts +57 -263
  113. package/src/api/FirmwareUpdateV4.ts +307 -912
  114. package/src/api/GetPassphraseState.ts +7 -1
  115. package/src/api/OpenWalletSession.ts +54 -17
  116. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  117. package/src/api/device/DeviceUnlock.ts +1 -1
  118. package/src/api/device/DeviceUpdateBootloader.ts +6 -122
  119. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -57
  120. package/src/api/firmware/updateBootloader.ts +4 -19
  121. package/src/api/firmware/uploadFirmware.ts +22 -140
  122. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  123. package/src/api/index.ts +1 -0
  124. package/src/api/protocol-v2/DeviceUploadNft.ts +189 -0
  125. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  126. package/src/core/deviceEventRegistration.ts +4 -0
  127. package/src/core/index.ts +136 -77
  128. package/src/data/messages/messages-protocol-v2.json +68 -0
  129. package/src/data-manager/DataManager.ts +63 -38
  130. package/src/data-manager/connectSettings.ts +0 -11
  131. package/src/device/Device.ts +203 -21
  132. package/src/events/device.ts +4 -0
  133. package/src/events/logBlockEvent.ts +1 -0
  134. package/src/events/ui-request.ts +33 -2
  135. package/src/index.ts +0 -5
  136. package/src/inject.ts +3 -22
  137. package/src/lowLevelInject.ts +1 -5
  138. package/src/protocols/protocol-v2/features.ts +9 -2
  139. package/src/protocols/protocol-v2/resources.ts +233 -0
  140. package/src/protocols/protocol-v2/uiInteraction.ts +46 -6
  141. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  142. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +124 -0
  143. package/src/protocols/protocol-v2/walletSession.ts +154 -30
  144. package/src/topLevelInject.ts +1 -5
  145. package/src/types/api/checkAllFirmwareRelease.ts +1 -4
  146. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  147. package/src/types/api/export.ts +0 -18
  148. package/src/types/api/firmwareUpdate.ts +0 -76
  149. package/src/types/api/index.ts +2 -14
  150. package/src/types/api/protocolV2.ts +13 -0
  151. package/src/types/settings.ts +26 -13
  152. package/src/utils/deviceFeaturesUtils.ts +4 -0
  153. package/src/utils/homescreen.ts +32 -6
  154. package/src/utils/index.ts +6 -1
  155. package/src/utils/pro2Nft.ts +142 -0
  156. package/src/utils/pro2Wallpaper.ts +35 -8
  157. package/src/utils/uiProgressThrottle.ts +63 -0
  158. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -175
  159. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  160. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  161. package/__tests__/firmware-update/firmware-preparation-error.test.ts +0 -27
  162. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  163. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -593
  164. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -213
  165. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +0 -58
  166. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  167. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  168. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  169. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  170. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  171. package/dist/api/firmware/FirmwarePreparationError.d.ts +0 -3
  172. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +0 -1
  173. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  174. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  175. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -25
  176. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  177. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -30
  178. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  179. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  180. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  181. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  182. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  183. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  184. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  185. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  186. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  187. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -28
  188. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  189. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  190. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  191. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  192. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  193. package/src/api/firmware/FirmwarePreparationError.ts +0 -12
  194. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  195. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -745
  196. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -392
  197. package/src/api/firmware/progressThrottle.ts +0 -44
  198. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  199. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
  200. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  201. package/src/types/api/firmwareUpdatePlan.ts +0 -38
  202. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -1,10 +1,14 @@
1
1
  import { EDeviceType, 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 GetPassphraseState from '../src/api/GetPassphraseState';
5
5
  import OpenWalletSession from '../src/api/OpenWalletSession';
6
6
  import { Device } from '../src/device/Device';
7
7
  import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
8
+ import {
9
+ ensureProtocolV2WalletSessionUnlocked,
10
+ getProtocolV2WalletSession,
11
+ } from '../src/protocols/protocol-v2/walletSession';
8
12
 
9
13
  jest.mock('../src/data/config', () => ({
10
14
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -26,6 +30,7 @@ const createDevice = ({
26
30
  unlocked: true,
27
31
  passphraseProtection,
28
32
  attachToPinEnabled: false,
33
+ unlockedAttachPin: false,
29
34
  },
30
35
  commands: {
31
36
  typedCall: jest.fn((request: string, ...args: unknown[]) => {
@@ -43,10 +48,21 @@ const createDevice = ({
43
48
  }),
44
49
  promptPassphrase,
45
50
  },
46
- getDeviceState: jest.fn().mockResolvedValue({
47
- identity: { deviceId: 'device-1' },
48
- status: { passphraseProtection },
49
- }),
51
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
52
+ device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {
53
+ eventless_wallet_session: true,
54
+ })
55
+ ),
56
+ getDeviceState: jest.fn(() =>
57
+ Promise.resolve({
58
+ identity: { deviceId: 'device-1' },
59
+ status: {
60
+ unlocked: device.features.unlocked,
61
+ passphraseProtection: device.features.passphraseProtection,
62
+ unlockedAttachPin: device.features.unlockedAttachPin,
63
+ },
64
+ })
65
+ ),
50
66
  getCurrentDeviceId: () => 'device-1',
51
67
  getCurrentPassphraseProtection: () => passphraseProtection,
52
68
  getInternalState: () =>
@@ -82,6 +98,109 @@ describe('openWalletSession', () => {
82
98
  deviceWalletSessionStore.clear();
83
99
  });
84
100
 
101
+ test('reuses the authoritative Main wallet DeviceStatus', async () => {
102
+ const typedCall = jest.fn((request: string) => {
103
+ if (request === 'ProtocolInfoRequest') {
104
+ return { message: { version: 2 } };
105
+ }
106
+ if (request === 'DeviceSessionGet') {
107
+ return {
108
+ message: {
109
+ btc_test_address: 'standard-state',
110
+ session_id: 'standard-session',
111
+ },
112
+ };
113
+ }
114
+ throw new Error(`Unexpected request: ${request}`);
115
+ });
116
+ const device = createDevice({ typedCall });
117
+
118
+ await expect(
119
+ getProtocolV2WalletSession(device as any, {
120
+ onlyMainPin: true,
121
+ })
122
+ ).resolves.toMatchObject({
123
+ passphraseState: 'standard-state',
124
+ newSession: 'standard-session',
125
+ });
126
+
127
+ expect(device.unlockDevice).not.toHaveBeenCalled();
128
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
129
+ seed_domains: [],
130
+ });
131
+ });
132
+
133
+ test('does not treat an Attach PIN DeviceStatus as the Main wallet', async () => {
134
+ const typedCall = jest.fn((request: string) => {
135
+ if (request === 'ProtocolInfoRequest') {
136
+ return { message: { version: 2 } };
137
+ }
138
+ if (request === 'DeviceSessionGet') {
139
+ return {
140
+ message: {
141
+ btc_test_address: 'standard-state',
142
+ session_id: 'standard-session',
143
+ },
144
+ };
145
+ }
146
+ throw new Error(`Unexpected request: ${request}`);
147
+ });
148
+ const device = createDevice({ typedCall });
149
+ device.features.unlockedAttachPin = true;
150
+
151
+ await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
152
+
153
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
154
+ source: 'wallet-session-coordinator',
155
+ reason: 'open-wallet',
156
+ deviceOnly: true,
157
+ });
158
+ });
159
+
160
+ test('unlocks a locked Protocol V2 device before restoring a wallet session', async () => {
161
+ const device = {
162
+ commands: {
163
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
164
+ },
165
+ isBootloader: jest.fn().mockReturnValue(false),
166
+ isProtocolV2: jest.fn().mockReturnValue(true),
167
+ isRomloader: jest.fn().mockReturnValue(false),
168
+ state: { status: { unlocked: false } },
169
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
170
+ updateProtocolV2Status: jest.fn(function updateProtocolV2Status(
171
+ this: { state: { status: { unlocked: boolean } } },
172
+ status: { unlocked?: boolean }
173
+ ) {
174
+ this.state.status.unlocked = status.unlocked ?? this.state.status.unlocked;
175
+ }),
176
+ };
177
+
178
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(true);
179
+ expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
180
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
181
+ source: 'unlock-coordinator',
182
+ reason: 'device-locked',
183
+ deviceOnly: true,
184
+ });
185
+ });
186
+
187
+ test('does not request PIN when wallet-session recovery finds the device unlocked', async () => {
188
+ const device = {
189
+ commands: {
190
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
191
+ },
192
+ isBootloader: jest.fn().mockReturnValue(false),
193
+ isProtocolV2: jest.fn().mockReturnValue(true),
194
+ isRomloader: jest.fn().mockReturnValue(false),
195
+ state: { status: { unlocked: true } },
196
+ unlockDevice: jest.fn(),
197
+ updateProtocolV2Status: jest.fn(),
198
+ };
199
+
200
+ await expect(ensureProtocolV2WalletSessionUnlocked(device as any)).resolves.toBe(false);
201
+ expect(device.unlockDevice).not.toHaveBeenCalled();
202
+ });
203
+
85
204
  test.each([{ useEmptyPassphrase: true }, { initSession: true }, {}])(
86
205
  'requires the explicit mode in the new public API: %p',
87
206
  legacyParams => {
@@ -133,7 +252,9 @@ describe('openWalletSession', () => {
133
252
  passphrase: 'host hidden wallet',
134
253
  on_device: false,
135
254
  });
136
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
255
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
256
+ seed_domains: [],
257
+ });
137
258
  });
138
259
 
139
260
  test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => {
@@ -251,7 +372,9 @@ describe('openWalletSession', () => {
251
372
  passphrase: 'host hidden wallet',
252
373
  on_device: false,
253
374
  });
254
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
375
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
376
+ seed_domains: [],
377
+ });
255
378
  });
256
379
 
257
380
  test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
@@ -273,10 +396,24 @@ describe('openWalletSession', () => {
273
396
  typedCall,
274
397
  promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
275
398
  });
276
- device.getDeviceState = jest.fn().mockResolvedValue({
277
- identity: { deviceId: 'device-1' },
278
- status: { unlocked: false, passphraseProtection: true },
279
- });
399
+ device.getDeviceState = jest
400
+ .fn()
401
+ .mockResolvedValueOnce({
402
+ identity: { deviceId: 'device-1' },
403
+ status: {
404
+ unlocked: false,
405
+ passphraseProtection: null,
406
+ unlockedAttachPin: null,
407
+ },
408
+ })
409
+ .mockResolvedValue({
410
+ identity: { deviceId: 'device-1' },
411
+ status: {
412
+ unlocked: true,
413
+ passphraseProtection: true,
414
+ unlockedAttachPin: false,
415
+ },
416
+ });
280
417
  method.device = device as any;
281
418
 
282
419
  await expect(method.run()).resolves.toMatchObject({
@@ -330,6 +467,12 @@ describe('openWalletSession', () => {
330
467
  status.passphrase_enabled ?? device.features.passphraseProtection;
331
468
  return device.features;
332
469
  });
470
+ device.unlockDevice = jest.fn().mockImplementation(() => {
471
+ device.features.unlocked = true;
472
+ device.features.passphraseProtection = true;
473
+ device.features.unlockedAttachPin = false;
474
+ return Promise.resolve(device.features);
475
+ });
333
476
  method.device = device as any;
334
477
 
335
478
  await expect(method.run()).resolves.toEqual({
@@ -339,10 +482,98 @@ describe('openWalletSession', () => {
339
482
  passphraseState: 'hidden-state-after-unlock',
340
483
  resumed: false,
341
484
  });
342
- expect(device.unlockDevice).not.toHaveBeenCalled();
485
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
343
486
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
344
487
  });
345
488
 
489
+ test('unlocks before wallet selection when the Protocol V2 wallet status is unknown', async () => {
490
+ const typedCall = jest
491
+ .fn()
492
+ .mockResolvedValueOnce({ message: { version: 2 } })
493
+ .mockResolvedValueOnce({ message: {} })
494
+ .mockResolvedValueOnce({
495
+ message: {
496
+ btc_test_address: 'hidden-state-after-unknown-status',
497
+ session_id: 'hidden-session-after-unknown-status',
498
+ },
499
+ });
500
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
501
+ const method = new OpenWalletSession({
502
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
503
+ });
504
+ method.init();
505
+ const device = createDevice({ typedCall, promptPassphrase });
506
+ device.getDeviceState = jest
507
+ .fn()
508
+ .mockResolvedValueOnce({
509
+ identity: { deviceId: 'device-1' },
510
+ status: {
511
+ unlocked: undefined,
512
+ passphraseProtection: null,
513
+ unlockedAttachPin: null,
514
+ },
515
+ })
516
+ .mockResolvedValue({
517
+ identity: { deviceId: 'device-1' },
518
+ status: {
519
+ unlocked: true,
520
+ passphraseProtection: true,
521
+ unlockedAttachPin: false,
522
+ },
523
+ });
524
+ device.unlockDevice = jest.fn().mockImplementation(() => {
525
+ device.features.unlocked = true;
526
+ device.features.passphraseProtection = true;
527
+ device.features.unlockedAttachPin = false;
528
+ return Promise.resolve(device.features);
529
+ });
530
+ method.device = device as any;
531
+
532
+ await expect(method.run()).resolves.toMatchObject({
533
+ walletType: 'hidden',
534
+ passphraseState: 'hidden-state-after-unknown-status',
535
+ });
536
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
537
+ expect(device.unlockDevice.mock.invocationCallOrder[0]).toBeLessThan(
538
+ promptPassphrase.mock.invocationCallOrder[0]
539
+ );
540
+ });
541
+
542
+ test('rejects wallet selection when the post-unlock Protocol V2 status remains incomplete', async () => {
543
+ const typedCall = jest
544
+ .fn()
545
+ .mockResolvedValueOnce({ message: { version: 2 } })
546
+ .mockResolvedValueOnce({ message: {} })
547
+ .mockResolvedValueOnce({
548
+ message: {
549
+ btc_test_address: 'hidden-state-from-incomplete-status',
550
+ session_id: 'hidden-session-from-incomplete-status',
551
+ },
552
+ });
553
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
554
+ const method = new OpenWalletSession({
555
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
556
+ });
557
+ method.init();
558
+ const device = createDevice({ typedCall, promptPassphrase });
559
+ device.getDeviceState = jest.fn().mockResolvedValue({
560
+ identity: { deviceId: 'device-1' },
561
+ status: {
562
+ unlocked: true,
563
+ passphraseProtection: null,
564
+ unlockedAttachPin: null,
565
+ },
566
+ });
567
+ device.unlockDevice = jest.fn().mockResolvedValue(device.features);
568
+ method.device = device as any;
569
+
570
+ await expect(method.run()).rejects.toMatchObject({
571
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
572
+ });
573
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
574
+ expect(promptPassphrase).not.toHaveBeenCalled();
575
+ });
576
+
346
577
  test('rejects a Pro2 standard-wallet fallback after selecting a hidden wallet', async () => {
347
578
  const typedCall = jest
348
579
  .fn()
@@ -408,7 +639,9 @@ describe('openWalletSession', () => {
408
639
  passphrase: 'host hidden wallet',
409
640
  on_device: false,
410
641
  });
411
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
642
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
643
+ seed_domains: [],
644
+ });
412
645
  expect(promptPassphrase).toHaveBeenCalled();
413
646
  expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
414
647
  });
@@ -479,6 +712,8 @@ describe('openWalletSession', () => {
479
712
  });
480
713
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
481
714
  session_id: 'known-session',
715
+ btc_test_address: 'hidden-state',
716
+ seed_domains: [],
482
717
  });
483
718
  expect(promptPassphrase).not.toHaveBeenCalled();
484
719
  });
@@ -656,8 +891,10 @@ describe('openWalletSession', () => {
656
891
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
657
892
  eventless_wallet_session: true,
658
893
  });
659
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
660
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
894
+ expect(device.unlockDevice).not.toHaveBeenCalled();
895
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
896
+ seed_domains: [],
897
+ });
661
898
  expect(promptPassphrase).not.toHaveBeenCalled();
662
899
  expect(device.passphraseState).toBeUndefined();
663
900
  });
@@ -686,7 +923,11 @@ describe('openWalletSession', () => {
686
923
  })
687
924
  .mockResolvedValueOnce({
688
925
  identity: { deviceId: 'device-1' },
689
- status: { passphraseProtection: true },
926
+ status: {
927
+ unlocked: true,
928
+ passphraseProtection: true,
929
+ unlockedAttachPin: false,
930
+ },
690
931
  });
691
932
  device.unlockDevice = jest.fn().mockImplementation(() => {
692
933
  device.features.unlocked = true;
@@ -701,7 +942,11 @@ describe('openWalletSession', () => {
701
942
  passphraseState: null,
702
943
  resumed: false,
703
944
  });
704
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
945
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
946
+ source: 'wallet-session-coordinator',
947
+ reason: 'open-wallet',
948
+ deviceOnly: true,
949
+ });
705
950
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
706
951
  });
707
952
 
@@ -721,6 +966,24 @@ describe('openWalletSession', () => {
721
966
  method.init();
722
967
  const device = createDevice({ typedCall });
723
968
  device.features.unlockedAttachPin = true;
969
+ device.getDeviceState = jest
970
+ .fn()
971
+ .mockResolvedValueOnce({
972
+ identity: { deviceId: 'device-1' },
973
+ status: {
974
+ unlocked: true,
975
+ unlockedAttachPin: true,
976
+ passphraseProtection: true,
977
+ },
978
+ })
979
+ .mockResolvedValue({
980
+ identity: { deviceId: 'device-1' },
981
+ status: {
982
+ unlocked: true,
983
+ unlockedAttachPin: false,
984
+ passphraseProtection: true,
985
+ },
986
+ });
724
987
  device.unlockDevice = jest.fn().mockImplementation(() => {
725
988
  device.features.unlockedAttachPin = false;
726
989
  return Promise.resolve(device.features);
@@ -731,8 +994,14 @@ describe('openWalletSession', () => {
731
994
  walletType: 'standard',
732
995
  passphraseState: null,
733
996
  });
734
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main);
735
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
997
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
998
+ source: 'wallet-session-coordinator',
999
+ reason: 'open-wallet',
1000
+ deviceOnly: true,
1001
+ });
1002
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1003
+ seed_domains: [],
1004
+ });
736
1005
  });
737
1006
 
738
1007
  test('selects a hidden wallet without exposing the internal device session', async () => {
@@ -766,7 +1035,9 @@ describe('openWalletSession', () => {
766
1035
  passphrase: 'host hidden wallet',
767
1036
  on_device: false,
768
1037
  });
769
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1038
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1039
+ seed_domains: [],
1040
+ });
770
1041
  expect(promptPassphrase).toHaveBeenCalled();
771
1042
  expect(device.passphraseState).toBeUndefined();
772
1043
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
@@ -795,7 +1066,9 @@ describe('openWalletSession', () => {
795
1066
  passphrase: 'host hidden wallet',
796
1067
  on_device: false,
797
1068
  });
798
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1069
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1070
+ seed_domains: [],
1071
+ });
799
1072
  });
800
1073
 
801
1074
  test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
@@ -820,7 +1093,9 @@ describe('openWalletSession', () => {
820
1093
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
821
1094
  on_device: true,
822
1095
  });
823
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
1096
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
1097
+ seed_domains: [],
1098
+ });
824
1099
  });
825
1100
 
826
1101
  test('forwards a host passphrase to Pro2 firmware', async () => {
@@ -862,7 +1137,9 @@ describe('openWalletSession', () => {
862
1137
  passphrase: 'host hidden wallet',
863
1138
  on_device: false,
864
1139
  });
865
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {});
1140
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
1141
+ seed_domains: [],
1142
+ });
866
1143
  });
867
1144
 
868
1145
  test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
@@ -939,8 +1216,12 @@ describe('openWalletSession', () => {
939
1216
  },
940
1217
  { cancelDeviceOnReject: false }
941
1218
  );
942
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
943
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1219
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
1220
+ emitUiEvent: false,
1221
+ });
1222
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1223
+ seed_domains: [],
1224
+ });
944
1225
  });
945
1226
 
946
1227
  test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
@@ -969,7 +1250,7 @@ describe('openWalletSession', () => {
969
1250
  ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
970
1251
  ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
971
1252
  ['DeviceStatusGet', 'DeviceStatus', {}],
972
- ['DeviceSessionGet', 'DeviceSession', {}],
1253
+ ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
973
1254
  ]);
974
1255
  expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
975
1256
  type: DeviceSessionPinType.Main,
@@ -1026,7 +1307,7 @@ describe('openWalletSession', () => {
1026
1307
 
1027
1308
  await expect(method.run()).rejects.toThrow(message);
1028
1309
  expect(typedCall).toHaveBeenCalledTimes(2);
1029
- expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1310
+ expect(typedCall.mock.calls.some(([name]) => name === 'DeviceSessionGet')).toBe(false);
1030
1311
  }
1031
1312
  );
1032
1313
 
@@ -1180,6 +1461,8 @@ describe('openWalletSession', () => {
1180
1461
  });
1181
1462
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1182
1463
  session_id: 'known-session',
1464
+ btc_test_address: 'hidden-state',
1465
+ seed_domains: [],
1183
1466
  });
1184
1467
  expect(promptPassphrase).not.toHaveBeenCalled();
1185
1468
  });
@@ -1215,7 +1498,11 @@ describe('openWalletSession', () => {
1215
1498
  })
1216
1499
  .mockResolvedValueOnce({
1217
1500
  identity: { deviceId: 'device-1' },
1218
- status: { passphraseProtection: true },
1501
+ status: {
1502
+ unlocked: true,
1503
+ passphraseProtection: true,
1504
+ unlockedAttachPin: false,
1505
+ },
1219
1506
  });
1220
1507
  device.unlockDevice = jest.fn().mockImplementation(() => {
1221
1508
  device.features.unlocked = true;
@@ -1234,6 +1521,8 @@ describe('openWalletSession', () => {
1234
1521
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
1235
1522
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1236
1523
  session_id: 'known-session',
1524
+ btc_test_address: 'hidden-state',
1525
+ seed_domains: [],
1237
1526
  });
1238
1527
  const sessionGetCall = typedCall.mock.calls.findIndex(
1239
1528
  ([requestName]) => requestName === 'DeviceSessionGet'
@@ -1271,7 +1560,11 @@ describe('openWalletSession', () => {
1271
1560
  })
1272
1561
  .mockResolvedValueOnce({
1273
1562
  identity: { deviceId: 'other-device' },
1274
- status: { passphraseProtection: true },
1563
+ status: {
1564
+ unlocked: true,
1565
+ passphraseProtection: true,
1566
+ unlockedAttachPin: false,
1567
+ },
1275
1568
  });
1276
1569
  device.unlockDevice = jest.fn().mockImplementation(() => {
1277
1570
  device.features.unlocked = true;
@@ -1326,6 +1619,8 @@ describe('openWalletSession', () => {
1326
1619
  });
1327
1620
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1328
1621
  session_id: 'expired-session',
1622
+ btc_test_address: 'hidden-state',
1623
+ seed_domains: [],
1329
1624
  });
1330
1625
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
1331
1626
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
@@ -1335,7 +1630,6 @@ describe('openWalletSession', () => {
1335
1630
  const typedCall = jest
1336
1631
  .fn()
1337
1632
  .mockResolvedValueOnce({ message: { version: 2 } })
1338
- .mockResolvedValueOnce({ message: {} })
1339
1633
  .mockResolvedValueOnce({
1340
1634
  message: {
1341
1635
  btc_test_address: 'hidden-state',
@@ -1362,7 +1656,48 @@ describe('openWalletSession', () => {
1362
1656
  passphraseState: 'hidden-state',
1363
1657
  resumed: false,
1364
1658
  });
1365
- expect(promptPassphrase).toHaveBeenCalledTimes(1);
1659
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1660
+ btc_test_address: 'hidden-state',
1661
+ seed_domains: [],
1662
+ });
1663
+ expect(promptPassphrase).not.toHaveBeenCalled();
1366
1664
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
1367
1665
  });
1666
+
1667
+ test.each([false, true])('keeps deriveCardano API compatibility: %s', async deriveCardano => {
1668
+ const typedCall = jest
1669
+ .fn()
1670
+ .mockResolvedValueOnce({ message: { version: 2 } })
1671
+ .mockResolvedValueOnce({ message: {} })
1672
+ .mockResolvedValueOnce({
1673
+ message: {
1674
+ btc_test_address: 'hidden-state',
1675
+ session_id: 'new-session',
1676
+ },
1677
+ });
1678
+ const method = new OpenWalletSession({
1679
+ payload: {
1680
+ method: 'openWalletSession',
1681
+ connectId: 'connect-id',
1682
+ mode: 'select-hidden',
1683
+ deriveCardano,
1684
+ },
1685
+ });
1686
+ method.init();
1687
+ method.device = createDevice({
1688
+ typedCall,
1689
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
1690
+ }) as any;
1691
+
1692
+ await expect(method.run()).resolves.toMatchObject({
1693
+ walletType: 'hidden',
1694
+ passphraseState: 'hidden-state',
1695
+ });
1696
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1697
+ seed_domains: [
1698
+ DeviceSessionSeedDomain.SeedDomain_Standard,
1699
+ ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
1700
+ ],
1701
+ });
1702
+ });
1368
1703
  });