@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.12

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 (79) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +13 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  4. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
  5. package/__tests__/device-lifecycle-events.test.ts +86 -0
  6. package/__tests__/deviceUploadNft.test.ts +68 -0
  7. package/__tests__/evmSignTypedData.test.ts +42 -0
  8. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
  9. package/__tests__/logBlockEvent.test.ts +45 -122
  10. package/__tests__/open-wallet-session.test.ts +781 -69
  11. package/__tests__/pro2HostAssetPackage.test.ts +165 -0
  12. package/__tests__/protocol-v2-resources.test.ts +7 -4
  13. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  14. package/__tests__/protocol-v2.test.ts +925 -97
  15. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  16. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  18. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  20. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  22. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +2 -2
  24. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  25. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  26. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  28. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  30. package/dist/core/RequestQueue.d.ts +2 -0
  31. package/dist/core/RequestQueue.d.ts.map +1 -1
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/device/Device.d.ts +1 -1
  35. package/dist/device/Device.d.ts.map +1 -1
  36. package/dist/device/DeviceConnector.d.ts +1 -1
  37. package/dist/device/DeviceConnector.d.ts.map +1 -1
  38. package/dist/events/logBlockEvent.d.ts.map +1 -1
  39. package/dist/events/ui-request.d.ts +3 -0
  40. package/dist/events/ui-request.d.ts.map +1 -1
  41. package/dist/index.d.ts +21 -5
  42. package/dist/index.js +2511 -1538
  43. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/settings.d.ts +2 -2
  49. package/dist/types/settings.d.ts.map +1 -1
  50. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  51. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  52. package/dist/utils/patch.d.ts +1 -1
  53. package/dist/utils/patch.d.ts.map +1 -1
  54. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  55. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  56. package/package.json +4 -4
  57. package/src/api/FirmwareUpdateV4.ts +474 -83
  58. package/src/api/OpenWalletSession.ts +39 -12
  59. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  60. package/src/api/evm/EVMSignTypedData.ts +1 -0
  61. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  62. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  63. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  64. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  65. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
  66. package/src/api/protocol-v2/helpers.ts +8 -0
  67. package/src/core/RequestQueue.ts +16 -1
  68. package/src/core/index.ts +164 -26
  69. package/src/data/messages/messages-protocol-v2.json +1589 -1375
  70. package/src/device/Device.ts +3 -1
  71. package/src/device/DeviceConnector.ts +7 -3
  72. package/src/events/logBlockEvent.ts +6 -75
  73. package/src/events/ui-request.ts +3 -0
  74. package/src/protocols/protocol-v2/resources.ts +9 -1
  75. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  76. package/src/protocols/protocol-v2/walletSession.ts +223 -56
  77. package/src/types/settings.ts +4 -2
  78. package/src/utils/deviceFeaturesUtils.ts +4 -0
  79. package/src/utils/pro2HostAssetPackage.ts +400 -0
@@ -11,6 +11,13 @@ import {
11
11
  getProtocolV2WalletSession,
12
12
  } from '../src/protocols/protocol-v2/walletSession';
13
13
 
14
+ const STANDARD_SEED_DOMAINS = [DeviceSessionSeedDomain.SeedDomain_Standard];
15
+ const CARDANO_SEED_DOMAINS = [
16
+ DeviceSessionSeedDomain.SeedDomain_Standard,
17
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
18
+ ];
19
+ const standardSessionGet = {};
20
+
14
21
  jest.mock('../src/data/config', () => ({
15
22
  getSDKVersion: jest.fn(() => '1.0.0'),
16
23
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
@@ -19,12 +26,16 @@ jest.mock('../src/data/config', () => ({
19
26
  const createDevice = ({
20
27
  passphraseProtection = true,
21
28
  unlockedAttachPin = false,
29
+ refreshedUnlocked = true,
30
+ refreshedPassphraseProtection = passphraseProtection,
22
31
  refreshedUnlockedAttachPin = unlockedAttachPin,
23
32
  typedCall = jest.fn(),
24
33
  promptPassphrase = jest.fn(),
25
34
  }: {
26
35
  passphraseProtection?: boolean;
27
36
  unlockedAttachPin?: boolean;
37
+ refreshedUnlocked?: boolean;
38
+ refreshedPassphraseProtection?: boolean;
28
39
  refreshedUnlockedAttachPin?: boolean;
29
40
  typedCall?: jest.Mock;
30
41
  promptPassphrase?: jest.Mock;
@@ -43,11 +54,11 @@ const createDevice = ({
43
54
  return {
44
55
  message: {
45
56
  device_id: 'device-1',
46
- unlocked: true,
57
+ unlocked: refreshedUnlocked,
47
58
  attach_to_pin_enabled: unlockedAttachPin,
48
59
  unlocked_attach_pin: refreshedUnlockedAttachPin,
49
60
  unlocked_by_attach_to_pin: refreshedUnlockedAttachPin,
50
- passphrase_enabled: passphraseProtection,
61
+ passphrase_enabled: refreshedPassphraseProtection,
51
62
  },
52
63
  };
53
64
  }
@@ -88,6 +99,8 @@ const createDevice = ({
88
99
  unlockDevice: jest.fn(),
89
100
  updateProtocolV2Status: jest.fn((status: Record<string, unknown>) => {
90
101
  device.features.unlocked = status.unlocked ?? device.features.unlocked;
102
+ device.features.passphraseProtection =
103
+ status.passphrase_enabled ?? device.features.passphraseProtection;
91
104
  device.features.attachToPinEnabled =
92
105
  status.attach_to_pin_enabled ?? device.features.attachToPinEnabled;
93
106
  device.features.unlockedAttachPin =
@@ -142,13 +155,12 @@ describe('openWalletSession', () => {
142
155
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
143
156
  passphrase: '',
144
157
  on_device: false,
158
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
145
159
  });
146
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
147
- seed_domains: [],
148
- });
160
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
149
161
  });
150
162
 
151
- test('opens the standard wallet directly when passphrase is disabled', async () => {
163
+ test('opens the already-unlocked standard wallet without repeating Main PIN when passphrase is disabled', async () => {
152
164
  const typedCall = jest.fn((request: string) => {
153
165
  if (request === 'ProtocolInfoRequest') {
154
166
  return { message: { version: 2 } };
@@ -167,14 +179,139 @@ describe('openWalletSession', () => {
167
179
 
168
180
  await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
169
181
 
182
+ expect(device.unlockDevice).not.toHaveBeenCalled();
170
183
  expect(typedCall).not.toHaveBeenCalledWith(
171
184
  'DeviceSessionAskPassphrase',
172
185
  'Success',
173
186
  expect.anything()
174
187
  );
175
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
176
- seed_domains: [],
188
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
189
+ });
190
+
191
+ test('refreshes stale wallet status before accepting an only-Main-PIN session', async () => {
192
+ let attachPinSelected = true;
193
+ const typedCall = jest.fn((request: string) => {
194
+ if (request === 'ProtocolInfoRequest') {
195
+ return { message: { version: 2 } };
196
+ }
197
+ if (request === 'DeviceSessionGet') {
198
+ return {
199
+ message: {
200
+ btc_test_address: 'standard-state',
201
+ session_id: 'standard-session',
202
+ },
203
+ };
204
+ }
205
+ if (request === 'DeviceSessionAskPassphrase') {
206
+ return { message: {} };
207
+ }
208
+ throw new Error(`Unexpected request: ${request}`);
209
+ });
210
+ const device = createDevice({
211
+ passphraseProtection: false,
212
+ unlockedAttachPin: false,
213
+ typedCall,
214
+ });
215
+ device.commands.typedCall.mockImplementation((request: string, ...args: unknown[]) => {
216
+ if (request === 'DeviceStatusGet') {
217
+ return {
218
+ message: {
219
+ device_id: 'device-1',
220
+ unlocked: true,
221
+ attach_to_pin_enabled: true,
222
+ unlocked_attach_pin: attachPinSelected,
223
+ unlocked_by_attach_to_pin: attachPinSelected,
224
+ passphrase_enabled: true,
225
+ },
226
+ };
227
+ }
228
+ return typedCall(request, ...args);
229
+ });
230
+ device.unlockDevice.mockImplementation(() => {
231
+ attachPinSelected = false;
232
+ device.features.unlockedAttachPin = false;
233
+ return Promise.resolve(device.features);
234
+ });
235
+
236
+ await expect(
237
+ getProtocolV2WalletSession(device as any, { onlyMainPin: true })
238
+ ).resolves.toMatchObject({
239
+ unlockedAttachPin: false,
240
+ });
241
+
242
+ expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
243
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
244
+ source: 'wallet-session-coordinator',
245
+ reason: 'open-wallet',
246
+ deviceOnly: true,
247
+ });
248
+ });
249
+
250
+ test('still requires Main PIN when a cached standard session resolves to another wallet', async () => {
251
+ let sessionGetCount = 0;
252
+ const typedCall = jest.fn((request: string) => {
253
+ if (request === 'ProtocolInfoRequest') {
254
+ return { message: { version: 2 } };
255
+ }
256
+ if (request === 'DeviceSessionGet') {
257
+ sessionGetCount += 1;
258
+ return {
259
+ message: {
260
+ btc_test_address:
261
+ sessionGetCount === 1 ? 'unexpected-wallet-state' : 'cached-standard-state',
262
+ session_id:
263
+ sessionGetCount === 1 ? 'unexpected-wallet-session' : 'cached-standard-session',
264
+ },
265
+ };
266
+ }
267
+ throw new Error(`Unexpected request: ${request}`);
268
+ });
269
+ const device = createDevice({ passphraseProtection: false, typedCall });
270
+ device.getStandardInternalState = jest.fn(() => ({
271
+ passphraseState: 'cached-standard-state',
272
+ sessionId: 'cached-standard-session',
273
+ }));
274
+ device.clearStandardInternalState = jest.fn();
275
+
276
+ await expect(
277
+ getProtocolV2WalletSession(device as any, { onlyMainPin: true })
278
+ ).resolves.toMatchObject({
279
+ passphraseState: 'cached-standard-state',
280
+ newSession: 'cached-standard-session',
281
+ });
282
+
283
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
284
+ source: 'wallet-session-coordinator',
285
+ reason: 'session-recovery',
286
+ deviceOnly: true,
287
+ });
288
+ expect(sessionGetCount).toBe(2);
289
+ });
290
+
291
+ test('reuses a Main PIN selected by the current preflight when passphrase is disabled', async () => {
292
+ const typedCall = jest.fn((request: string) => {
293
+ if (request === 'ProtocolInfoRequest') {
294
+ return { message: { version: 2 } };
295
+ }
296
+ if (request === 'DeviceSessionGet') {
297
+ return {
298
+ message: {
299
+ btc_test_address: 'standard-state',
300
+ session_id: 'standard-session',
301
+ },
302
+ };
303
+ }
304
+ throw new Error(`Unexpected request: ${request}`);
305
+ });
306
+ const device = createDevice({ passphraseProtection: false, typedCall });
307
+
308
+ await getProtocolV2WalletSession(device as any, {
309
+ onlyMainPin: true,
310
+ mainPinSelected: true,
177
311
  });
312
+
313
+ expect(device.unlockDevice).not.toHaveBeenCalled();
314
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
178
315
  });
179
316
 
180
317
  test('does not treat an Attach PIN DeviceStatus as the Main wallet', async () => {
@@ -195,8 +332,7 @@ describe('openWalletSession', () => {
195
332
  }
196
333
  throw new Error(`Unexpected request: ${request}`);
197
334
  });
198
- const device = createDevice({ typedCall });
199
- device.features.unlockedAttachPin = true;
335
+ const device = createDevice({ refreshedUnlockedAttachPin: true, typedCall });
200
336
 
201
337
  await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
202
338
 
@@ -301,10 +437,14 @@ describe('openWalletSession', () => {
301
437
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
302
438
  passphrase: 'host hidden wallet',
303
439
  on_device: false,
440
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
304
441
  });
305
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
306
- seed_domains: [],
307
- });
442
+ expect(typedCall).toHaveBeenNthCalledWith(
443
+ 3,
444
+ 'DeviceSessionGet',
445
+ 'DeviceSession',
446
+ standardSessionGet
447
+ );
308
448
  });
309
449
 
310
450
  test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => {
@@ -421,10 +561,14 @@ describe('openWalletSession', () => {
421
561
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
422
562
  passphrase: 'host hidden wallet',
423
563
  on_device: false,
564
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
424
565
  });
425
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
426
- seed_domains: [],
427
- });
566
+ expect(typedCall).toHaveBeenNthCalledWith(
567
+ 3,
568
+ 'DeviceSessionGet',
569
+ 'DeviceSession',
570
+ standardSessionGet
571
+ );
428
572
  });
429
573
 
430
574
  test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
@@ -698,9 +842,11 @@ describe('openWalletSession', () => {
698
842
  });
699
843
  expect(promptPassphrase).not.toHaveBeenCalled();
700
844
  expect(device.unlockDevice).not.toHaveBeenCalled();
701
- expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
702
- seed_domains: [],
703
- });
845
+ expect(device.commands.typedCall).toHaveBeenCalledWith(
846
+ 'DeviceSessionGet',
847
+ 'DeviceSession',
848
+ standardSessionGet
849
+ );
704
850
  expect(device.commands.typedCall).not.toHaveBeenCalledWith(
705
851
  'DeviceSessionAskPassphrase',
706
852
  'Success',
@@ -796,10 +942,9 @@ describe('openWalletSession', () => {
796
942
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
797
943
  passphrase: 'host hidden wallet',
798
944
  on_device: false,
945
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
799
946
  });
800
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
801
- seed_domains: [],
802
- });
947
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
803
948
  expect(promptPassphrase).toHaveBeenCalled();
804
949
  expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
805
950
  });
@@ -871,7 +1016,7 @@ describe('openWalletSession', () => {
871
1016
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
872
1017
  session_id: 'known-session',
873
1018
  btc_test_address: 'hidden-state',
874
- seed_domains: [],
1019
+ ...standardSessionGet,
875
1020
  });
876
1021
  expect(promptPassphrase).not.toHaveBeenCalled();
877
1022
  });
@@ -1054,10 +1199,9 @@ describe('openWalletSession', () => {
1054
1199
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1055
1200
  passphrase: '',
1056
1201
  on_device: false,
1202
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1057
1203
  });
1058
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1059
- seed_domains: [],
1060
- });
1204
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1061
1205
  expect(promptPassphrase).not.toHaveBeenCalled();
1062
1206
  expect(device.passphraseState).toBeUndefined();
1063
1207
  });
@@ -1077,7 +1221,7 @@ describe('openWalletSession', () => {
1077
1221
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
1078
1222
  });
1079
1223
  method.init();
1080
- const device = createDevice({ typedCall });
1224
+ const device = createDevice({ refreshedUnlocked: false, typedCall });
1081
1225
  device.features.unlocked = false;
1082
1226
  device.getDeviceState = jest
1083
1227
  .fn()
@@ -1114,7 +1258,70 @@ describe('openWalletSession', () => {
1114
1258
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
1115
1259
  });
1116
1260
 
1261
+ test('does not restrict a locked hidden-wallet selection to the Main PIN', async () => {
1262
+ const typedCall = jest
1263
+ .fn()
1264
+ .mockResolvedValueOnce({ message: { version: 2 } })
1265
+ .mockResolvedValueOnce({ message: {} })
1266
+ .mockResolvedValueOnce({
1267
+ message: {
1268
+ btc_test_address: 'hidden-state',
1269
+ session_id: 'hidden-session',
1270
+ },
1271
+ });
1272
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1273
+ const method = new OpenWalletSession({
1274
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1275
+ });
1276
+ method.init();
1277
+ const device = createDevice({ typedCall, promptPassphrase });
1278
+ device.features.unlocked = false;
1279
+ device.getDeviceState = jest
1280
+ .fn()
1281
+ .mockResolvedValueOnce({
1282
+ identity: { deviceId: undefined },
1283
+ status: { unlocked: false, passphraseProtection: true },
1284
+ })
1285
+ .mockResolvedValueOnce({
1286
+ identity: { deviceId: 'device-1' },
1287
+ status: {
1288
+ unlocked: true,
1289
+ passphraseProtection: true,
1290
+ unlockedAttachPin: false,
1291
+ },
1292
+ })
1293
+ .mockResolvedValueOnce({
1294
+ identity: { deviceId: 'device-1' },
1295
+ status: {
1296
+ unlocked: true,
1297
+ passphraseProtection: true,
1298
+ unlockedAttachPin: false,
1299
+ },
1300
+ });
1301
+ device.unlockDevice = jest.fn().mockImplementation(() => {
1302
+ device.features.unlocked = true;
1303
+ return Promise.resolve(device.features);
1304
+ });
1305
+ method.device = device as any;
1306
+
1307
+ await expect(method.run()).resolves.toMatchObject({
1308
+ walletType: 'hidden',
1309
+ passphraseState: 'hidden-state',
1310
+ });
1311
+ expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, {
1312
+ source: 'unlock-coordinator',
1313
+ reason: 'device-locked',
1314
+ deviceOnly: true,
1315
+ method: 'openWalletSession',
1316
+ });
1317
+ expect(device.unlockDevice).not.toHaveBeenCalledWith(
1318
+ DeviceSessionPinType.Main,
1319
+ expect.anything()
1320
+ );
1321
+ });
1322
+
1117
1323
  test('switches from Attach PIN to Main PIN before opening the standard wallet', async () => {
1324
+ let attachPinSelected = true;
1118
1325
  const typedCall = jest
1119
1326
  .fn()
1120
1327
  .mockResolvedValueOnce({ message: { version: 2 } })
@@ -1131,6 +1338,21 @@ describe('openWalletSession', () => {
1131
1338
  method.init();
1132
1339
  const device = createDevice({ typedCall });
1133
1340
  device.features.unlockedAttachPin = true;
1341
+ device.commands.typedCall.mockImplementation((request: string, ...args: unknown[]) => {
1342
+ if (request === 'DeviceStatusGet') {
1343
+ return {
1344
+ message: {
1345
+ device_id: 'device-1',
1346
+ unlocked: true,
1347
+ attach_to_pin_enabled: true,
1348
+ unlocked_attach_pin: attachPinSelected,
1349
+ unlocked_by_attach_to_pin: attachPinSelected,
1350
+ passphrase_enabled: true,
1351
+ },
1352
+ };
1353
+ }
1354
+ return typedCall(request, ...args);
1355
+ });
1134
1356
  device.getDeviceState = jest
1135
1357
  .fn()
1136
1358
  .mockResolvedValueOnce({
@@ -1150,6 +1372,7 @@ describe('openWalletSession', () => {
1150
1372
  },
1151
1373
  });
1152
1374
  device.unlockDevice = jest.fn().mockImplementation(() => {
1375
+ attachPinSelected = false;
1153
1376
  device.features.unlockedAttachPin = false;
1154
1377
  return Promise.resolve(device.features);
1155
1378
  });
@@ -1164,9 +1387,7 @@ describe('openWalletSession', () => {
1164
1387
  reason: 'open-wallet',
1165
1388
  deviceOnly: true,
1166
1389
  });
1167
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1168
- seed_domains: [],
1169
- });
1390
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1170
1391
  });
1171
1392
 
1172
1393
  test('selects a hidden wallet without exposing the internal device session', async () => {
@@ -1199,15 +1420,45 @@ describe('openWalletSession', () => {
1199
1420
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1200
1421
  passphrase: 'host hidden wallet',
1201
1422
  on_device: false,
1423
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1202
1424
  });
1203
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1204
- seed_domains: [],
1205
- });
1425
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1206
1426
  expect(promptPassphrase).toHaveBeenCalled();
1207
1427
  expect(device.passphraseState).toBeUndefined();
1208
1428
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
1209
1429
  });
1210
1430
 
1431
+ test('reuses the fresh unlock and wallet-selection statuses', async () => {
1432
+ const typedCall = jest
1433
+ .fn()
1434
+ .mockResolvedValueOnce({ message: { version: 2 } })
1435
+ .mockResolvedValueOnce({ message: {} })
1436
+ .mockResolvedValueOnce({
1437
+ message: {
1438
+ btc_test_address: 'hidden-state',
1439
+ session_id: 'hidden-session',
1440
+ },
1441
+ });
1442
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1443
+ const method = new OpenWalletSession({
1444
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
1445
+ });
1446
+ method.init();
1447
+ method.protocolV2UnlockContext = {
1448
+ preflightCompleted: true,
1449
+ preflightStatusRefreshed: true,
1450
+ };
1451
+ const device = createDevice({ typedCall, promptPassphrase });
1452
+ method.device = device as any;
1453
+
1454
+ await expect(method.run()).resolves.toMatchObject({
1455
+ walletType: 'hidden',
1456
+ passphraseState: 'hidden-state',
1457
+ });
1458
+
1459
+ expect(device.getDeviceState.mock.calls).toEqual([[], []]);
1460
+ });
1461
+
1211
1462
  test('gets the prepared session after host passphrase entry', async () => {
1212
1463
  const typedCall = jest
1213
1464
  .fn()
@@ -1230,10 +1481,9 @@ describe('openWalletSession', () => {
1230
1481
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1231
1482
  passphrase: 'host hidden wallet',
1232
1483
  on_device: false,
1484
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1233
1485
  });
1234
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1235
- seed_domains: [],
1236
- });
1486
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1237
1487
  });
1238
1488
 
1239
1489
  test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
@@ -1278,10 +1528,14 @@ describe('openWalletSession', () => {
1278
1528
  });
1279
1529
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1280
1530
  on_device: true,
1531
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1281
1532
  });
1282
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
1283
- seed_domains: [],
1284
- });
1533
+ expect(typedCall).toHaveBeenNthCalledWith(
1534
+ 3,
1535
+ 'DeviceSessionGet',
1536
+ 'DeviceSession',
1537
+ standardSessionGet
1538
+ );
1285
1539
  expect(device.createProtocolV2UiPhaseMetadata).toHaveBeenNthCalledWith(
1286
1540
  2,
1287
1541
  'passphrase-on-device',
@@ -1332,10 +1586,14 @@ describe('openWalletSession', () => {
1332
1586
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1333
1587
  passphrase: 'host hidden wallet',
1334
1588
  on_device: false,
1589
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1335
1590
  });
1336
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
1337
- seed_domains: [],
1338
- });
1591
+ expect(typedCall).toHaveBeenNthCalledWith(
1592
+ 3,
1593
+ 'DeviceSessionGet',
1594
+ 'DeviceSession',
1595
+ standardSessionGet
1596
+ );
1339
1597
  });
1340
1598
 
1341
1599
  test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
@@ -1357,6 +1615,7 @@ describe('openWalletSession', () => {
1357
1615
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1358
1616
  passphrase: 'cafe\u0301',
1359
1617
  on_device: false,
1618
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1360
1619
  });
1361
1620
  });
1362
1621
 
@@ -1380,6 +1639,7 @@ describe('openWalletSession', () => {
1380
1639
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1381
1640
  passphrase,
1382
1641
  on_device: false,
1642
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1383
1643
  });
1384
1644
  });
1385
1645
 
@@ -1415,9 +1675,7 @@ describe('openWalletSession', () => {
1415
1675
  expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
1416
1676
  emitUiEvent: false,
1417
1677
  });
1418
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1419
- seed_domains: [],
1420
- });
1678
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1421
1679
  });
1422
1680
 
1423
1681
  test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
@@ -1446,7 +1704,7 @@ describe('openWalletSession', () => {
1446
1704
  ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1447
1705
  ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
1448
1706
  ['DeviceStatusGet', 'DeviceStatus', {}],
1449
- ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
1707
+ ['DeviceSessionGet', 'DeviceSession', standardSessionGet],
1450
1708
  ]);
1451
1709
  expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
1452
1710
  type: DeviceSessionPinType.Main,
@@ -1658,7 +1916,7 @@ describe('openWalletSession', () => {
1658
1916
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1659
1917
  session_id: 'known-session',
1660
1918
  btc_test_address: 'hidden-state',
1661
- seed_domains: [],
1919
+ ...standardSessionGet,
1662
1920
  });
1663
1921
  expect(promptPassphrase).not.toHaveBeenCalled();
1664
1922
  });
@@ -1718,7 +1976,7 @@ describe('openWalletSession', () => {
1718
1976
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1719
1977
  session_id: 'known-session',
1720
1978
  btc_test_address: 'hidden-state',
1721
- seed_domains: [],
1979
+ ...standardSessionGet,
1722
1980
  });
1723
1981
  const sessionGetCall = typedCall.mock.calls.findIndex(
1724
1982
  ([requestName]) => requestName === 'DeviceSessionGet'
@@ -1816,23 +2074,28 @@ describe('openWalletSession', () => {
1816
2074
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1817
2075
  session_id: 'expired-session',
1818
2076
  btc_test_address: 'hidden-state',
1819
- seed_domains: [],
2077
+ ...standardSessionGet,
1820
2078
  });
1821
2079
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
1822
2080
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
1823
2081
  });
1824
2082
 
1825
- test('selects the expected hidden wallet when Protocol V2 has no cached session', async () => {
2083
+ test('recovers the current Attach PIN wallet when the cached session is invalid', async () => {
1826
2084
  const typedCall = jest
1827
2085
  .fn()
1828
2086
  .mockResolvedValueOnce({ message: { version: 2 } })
2087
+ .mockRejectedValueOnce(
2088
+ Object.assign(new Error('Invalid session'), {
2089
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
2090
+ })
2091
+ )
1829
2092
  .mockResolvedValueOnce({
1830
2093
  message: {
1831
2094
  btc_test_address: 'hidden-state',
1832
- session_id: 'new-session',
2095
+ session_id: 'renewed-session',
1833
2096
  },
1834
2097
  });
1835
- const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
2098
+ const promptPassphrase = jest.fn();
1836
2099
  const method = new OpenWalletSession({
1837
2100
  payload: {
1838
2101
  method: 'openWalletSession',
@@ -1843,7 +2106,9 @@ describe('openWalletSession', () => {
1843
2106
  },
1844
2107
  });
1845
2108
  method.init();
1846
- method.device = createDevice({ typedCall, promptPassphrase }) as any;
2109
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'expired-session');
2110
+ const device = createDevice({ unlockedAttachPin: true, typedCall, promptPassphrase });
2111
+ method.device = device as any;
1847
2112
 
1848
2113
  await expect(method.run()).resolves.toEqual({
1849
2114
  protocol: 'V2',
@@ -1852,48 +2117,495 @@ describe('openWalletSession', () => {
1852
2117
  passphraseState: 'hidden-state',
1853
2118
  resumed: false,
1854
2119
  });
1855
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1856
- btc_test_address: 'hidden-state',
1857
- seed_domains: [],
1858
- });
2120
+ expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toEqual([
2121
+ [
2122
+ 'DeviceSessionGet',
2123
+ 'DeviceSession',
2124
+ {
2125
+ session_id: 'expired-session',
2126
+ btc_test_address: 'hidden-state',
2127
+ },
2128
+ ],
2129
+ ['DeviceSessionGet', 'DeviceSession', {}],
2130
+ ]);
1859
2131
  expect(promptPassphrase).not.toHaveBeenCalled();
1860
- expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
2132
+ expect(device.lockDevice).not.toHaveBeenCalled();
2133
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
1861
2134
  });
1862
2135
 
1863
- test.each([false, true])('keeps deriveCardano API compatibility: %s', async deriveCardano => {
2136
+ test('selects the expected hidden wallet when Protocol V2 has no cached session', async () => {
1864
2137
  const typedCall = jest
1865
2138
  .fn()
1866
2139
  .mockResolvedValueOnce({ message: { version: 2 } })
1867
- .mockResolvedValueOnce({ message: {} })
1868
2140
  .mockResolvedValueOnce({
1869
2141
  message: {
1870
2142
  btc_test_address: 'hidden-state',
1871
2143
  session_id: 'new-session',
1872
2144
  },
1873
2145
  });
2146
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
1874
2147
  const method = new OpenWalletSession({
1875
2148
  payload: {
1876
2149
  method: 'openWalletSession',
1877
2150
  connectId: 'connect-id',
1878
- mode: 'select-hidden',
1879
- deriveCardano,
1880
- },
1881
- });
2151
+ mode: 'resume-hidden',
2152
+ deviceId: 'device-1',
2153
+ passphraseState: 'hidden-state',
2154
+ },
2155
+ });
1882
2156
  method.init();
1883
- method.device = createDevice({
2157
+ method.device = createDevice({ typedCall, promptPassphrase }) as any;
2158
+
2159
+ await expect(method.run()).resolves.toEqual({
2160
+ protocol: 'V2',
2161
+ walletType: 'hidden',
2162
+ deviceId: 'device-1',
2163
+ passphraseState: 'hidden-state',
2164
+ resumed: false,
2165
+ });
2166
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2167
+ btc_test_address: 'hidden-state',
2168
+ ...standardSessionGet,
2169
+ });
2170
+ expect(promptPassphrase).not.toHaveBeenCalled();
2171
+ expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
2172
+ });
2173
+
2174
+ test.each([false, true])(
2175
+ 'openWalletSession does not thread CommonParams.deriveCardano: %s',
2176
+ async deriveCardano => {
2177
+ const typedCall = jest
2178
+ .fn()
2179
+ .mockResolvedValueOnce({ message: { version: 2 } })
2180
+ .mockResolvedValueOnce({ message: {} })
2181
+ .mockResolvedValueOnce({
2182
+ message: {
2183
+ btc_test_address: 'hidden-state',
2184
+ session_id: 'new-session',
2185
+ },
2186
+ });
2187
+ const method = new OpenWalletSession({
2188
+ payload: {
2189
+ method: 'openWalletSession',
2190
+ connectId: 'connect-id',
2191
+ mode: 'select-hidden',
2192
+ deriveCardano,
2193
+ },
2194
+ });
2195
+ method.init();
2196
+ method.device = createDevice({
2197
+ typedCall,
2198
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2199
+ }) as any;
2200
+
2201
+ await expect(method.run()).resolves.toMatchObject({
2202
+ walletType: 'hidden',
2203
+ passphraseState: 'hidden-state',
2204
+ });
2205
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2206
+ passphrase: 'host hidden wallet',
2207
+ on_device: false,
2208
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2209
+ });
2210
+ expect(typedCall).toHaveBeenCalledWith(
2211
+ 'DeviceSessionGet',
2212
+ 'DeviceSession',
2213
+ standardSessionGet
2214
+ );
2215
+ }
2216
+ );
2217
+
2218
+ test('requests Cardano seed domains when a V2 session rebuild has Cardano intent', async () => {
2219
+ const typedCall = jest.fn((request: string) => {
2220
+ if (request === 'ProtocolInfoRequest') {
2221
+ return { message: { version: 2 } };
2222
+ }
2223
+ if (request === 'DeviceSessionAskPassphrase') {
2224
+ return { message: {} };
2225
+ }
2226
+ if (request === 'DeviceSessionGet') {
2227
+ return {
2228
+ message: {
2229
+ btc_test_address: 'hidden-state',
2230
+ session_id: 'new-session',
2231
+ seed_domains: [
2232
+ DeviceSessionSeedDomain.SeedDomain_Standard,
2233
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
2234
+ ],
2235
+ },
2236
+ };
2237
+ }
2238
+ throw new Error(`Unexpected request: ${request}`);
2239
+ });
2240
+ const device = createDevice({
1884
2241
  typedCall,
1885
2242
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
1886
- }) as any;
2243
+ });
1887
2244
 
1888
- await expect(method.run()).resolves.toMatchObject({
1889
- walletType: 'hidden',
2245
+ await expect(
2246
+ getProtocolV2WalletSession(device as any, {
2247
+ forceWalletSelection: true,
2248
+ deriveCardano: true,
2249
+ })
2250
+ ).resolves.toMatchObject({
1890
2251
  passphraseState: 'hidden-state',
2252
+ newSession: 'new-session',
2253
+ });
2254
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2255
+ passphrase: 'host hidden wallet',
2256
+ on_device: false,
2257
+ seed_domains: CARDANO_SEED_DOMAINS,
1891
2258
  });
2259
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
2260
+ });
2261
+
2262
+ test('asks Cardano seed domains after Get reports a Standard-only session', async () => {
2263
+ const typedCall = jest.fn((request: string) => {
2264
+ if (request === 'ProtocolInfoRequest') {
2265
+ return { message: { version: 2 } };
2266
+ }
2267
+ if (request === 'DeviceSessionAskPassphrase') {
2268
+ return { message: {} };
2269
+ }
2270
+ if (request === 'DeviceSessionGet') {
2271
+ const hasCardanoAsk = typedCall.mock.calls.some(
2272
+ call =>
2273
+ call[0] === 'DeviceSessionAskPassphrase' &&
2274
+ Array.isArray(call[2]?.seed_domains) &&
2275
+ call[2].seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano)
2276
+ );
2277
+ return {
2278
+ message: {
2279
+ btc_test_address: 'hidden-state',
2280
+ session_id: 'new-session',
2281
+ seed_domains: hasCardanoAsk
2282
+ ? [
2283
+ DeviceSessionSeedDomain.SeedDomain_Standard,
2284
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
2285
+ ]
2286
+ : [DeviceSessionSeedDomain.SeedDomain_Standard],
2287
+ },
2288
+ };
2289
+ }
2290
+ throw new Error(`Unexpected request: ${request}`);
2291
+ });
2292
+ const device = createDevice({
2293
+ typedCall,
2294
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2295
+ });
2296
+ device.passphraseState = 'hidden-state';
2297
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'cached-session');
2298
+
2299
+ await getProtocolV2WalletSession(device as any, {
2300
+ expectedPassphraseState: 'hidden-state',
2301
+ deriveCardano: true,
2302
+ });
2303
+
1892
2304
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2305
+ session_id: 'cached-session',
2306
+ btc_test_address: 'hidden-state',
2307
+ });
2308
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2309
+ passphrase: 'host hidden wallet',
2310
+ on_device: false,
1893
2311
  seed_domains: [
1894
2312
  DeviceSessionSeedDomain.SeedDomain_Standard,
1895
- ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
2313
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
1896
2314
  ],
1897
2315
  });
1898
2316
  });
2317
+
2318
+ test('uses empty AskPassphrase to add Cardano on an Attach PIN session', async () => {
2319
+ const typedCall = jest.fn((request: string) => {
2320
+ if (request === 'ProtocolInfoRequest') {
2321
+ return { message: { version: 2 } };
2322
+ }
2323
+ if (request === 'DeviceSessionAskPassphrase') {
2324
+ return { message: {} };
2325
+ }
2326
+ if (request === 'DeviceSessionGet') {
2327
+ const askedCardano = typedCall.mock.calls.some(
2328
+ call =>
2329
+ call[0] === 'DeviceSessionAskPassphrase' &&
2330
+ Array.isArray(call[2]?.seed_domains) &&
2331
+ call[2].seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano)
2332
+ );
2333
+ return {
2334
+ message: {
2335
+ btc_test_address: 'attach-state',
2336
+ session_id: 'attach-session',
2337
+ seed_domains: askedCardano ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS,
2338
+ },
2339
+ };
2340
+ }
2341
+ throw new Error(`Unexpected request: ${request}`);
2342
+ });
2343
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' });
2344
+ const device = createDevice({
2345
+ typedCall,
2346
+ promptPassphrase,
2347
+ unlockedAttachPin: true,
2348
+ });
2349
+
2350
+ await expect(
2351
+ getProtocolV2WalletSession(device as any, {
2352
+ readCurrentAttachPinSession: true,
2353
+ deriveCardano: true,
2354
+ })
2355
+ ).resolves.toMatchObject({
2356
+ passphraseState: 'attach-state',
2357
+ newSession: 'attach-session',
2358
+ unlockedAttachPin: true,
2359
+ });
2360
+ expect(promptPassphrase).not.toHaveBeenCalled();
2361
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2362
+ passphrase: '',
2363
+ on_device: false,
2364
+ seed_domains: CARDANO_SEED_DOMAINS,
2365
+ });
2366
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2367
+ expect(
2368
+ typedCall.mock.calls
2369
+ .filter(call => call[0] === 'DeviceSessionGet')
2370
+ .every(call => !('seed_domains' in (call[2] ?? {})))
2371
+ ).toBe(true);
2372
+ });
2373
+
2374
+ test('uses empty AskPassphrase after selecting Attach PIN for Cardano intent', async () => {
2375
+ const typedCall = jest.fn((request: string) => {
2376
+ if (request === 'ProtocolInfoRequest') {
2377
+ return { message: { version: 2 } };
2378
+ }
2379
+ if (request === 'DeviceSessionAskPassphrase') {
2380
+ return { message: {} };
2381
+ }
2382
+ if (request === 'DeviceSessionGet') {
2383
+ return {
2384
+ message: {
2385
+ btc_test_address: 'attach-state',
2386
+ session_id: 'attach-session',
2387
+ seed_domains: CARDANO_SEED_DOMAINS,
2388
+ },
2389
+ };
2390
+ }
2391
+ throw new Error(`Unexpected request: ${request}`);
2392
+ });
2393
+ const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
2394
+ const device = createDevice({ typedCall, promptPassphrase });
2395
+ device.features.attachToPinEnabled = true;
2396
+
2397
+ await expect(
2398
+ getProtocolV2WalletSession(device as any, {
2399
+ forceWalletSelection: true,
2400
+ deriveCardano: true,
2401
+ })
2402
+ ).resolves.toMatchObject({
2403
+ passphraseState: 'attach-state',
2404
+ newSession: 'attach-session',
2405
+ unlockedAttachPin: true,
2406
+ });
2407
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
2408
+ expect(device.unlockDevice).toHaveBeenCalledWith(
2409
+ DeviceSessionPinType.AttachToPin,
2410
+ expect.objectContaining({ emitUiEvent: false })
2411
+ );
2412
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2413
+ passphrase: '',
2414
+ on_device: false,
2415
+ seed_domains: CARDANO_SEED_DOMAINS,
2416
+ });
2417
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2418
+ expect(
2419
+ typedCall.mock.calls
2420
+ .filter(call => call[0] === 'DeviceSessionGet')
2421
+ .every(call => !('seed_domains' in (call[2] ?? {})))
2422
+ ).toBe(true);
2423
+ });
2424
+
2425
+ test('still locks before a passphrase picker after Attach PIN Cardano empty Ask', async () => {
2426
+ const typedCall = jest.fn((request: string) => {
2427
+ if (request === 'ProtocolInfoRequest') {
2428
+ return { message: { version: 2 } };
2429
+ }
2430
+ if (request === 'DeviceSessionAskPassphrase') {
2431
+ return { message: {} };
2432
+ }
2433
+ if (request === 'DeviceSessionGet') {
2434
+ return {
2435
+ message: {
2436
+ btc_test_address: 'attach-state',
2437
+ session_id: 'attach-session',
2438
+ seed_domains: CARDANO_SEED_DOMAINS,
2439
+ },
2440
+ };
2441
+ }
2442
+ throw new Error(`Unexpected request: ${request}`);
2443
+ });
2444
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' });
2445
+ const device = createDevice({
2446
+ typedCall,
2447
+ promptPassphrase,
2448
+ unlockedAttachPin: true,
2449
+ });
2450
+
2451
+ await getProtocolV2WalletSession(device as any, {
2452
+ readCurrentAttachPinSession: true,
2453
+ deriveCardano: true,
2454
+ });
2455
+ await expect(
2456
+ getProtocolV2WalletSession(device as any, { forceWalletSelection: true })
2457
+ ).rejects.toMatchObject({
2458
+ errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
2459
+ });
2460
+ expect(device.lockDevice).toHaveBeenCalled();
2461
+ expect(promptPassphrase).not.toHaveBeenCalled();
2462
+ });
2463
+
2464
+ test('uses a second Get for Cardano when passphrase protection is off', async () => {
2465
+ const typedCall = jest.fn((request: string) => {
2466
+ if (request === 'ProtocolInfoRequest') {
2467
+ return { message: { version: 2 } };
2468
+ }
2469
+ if (request === 'DeviceSessionGet') {
2470
+ const getCount = typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet').length;
2471
+ return {
2472
+ message: {
2473
+ btc_test_address: 'standard-state',
2474
+ session_id: 'standard-session',
2475
+ seed_domains: getCount > 1 ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS,
2476
+ },
2477
+ };
2478
+ }
2479
+ throw new Error(`Unexpected request: ${request}`);
2480
+ });
2481
+ const device = createDevice({
2482
+ typedCall,
2483
+ passphraseProtection: false,
2484
+ });
2485
+
2486
+ await expect(
2487
+ getProtocolV2WalletSession(device as any, {
2488
+ onlyMainPin: true,
2489
+ deriveCardano: true,
2490
+ })
2491
+ ).resolves.toMatchObject({
2492
+ passphraseState: 'standard-state',
2493
+ newSession: 'standard-session',
2494
+ });
2495
+ expect(typedCall).not.toHaveBeenCalledWith(
2496
+ 'DeviceSessionAskPassphrase',
2497
+ 'Success',
2498
+ expect.anything()
2499
+ );
2500
+ expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(2);
2501
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2502
+ });
2503
+
2504
+ test('fails closed when Cardano fallback still lacks Cardano', async () => {
2505
+ const typedCall = jest.fn((request: string) => {
2506
+ if (request === 'ProtocolInfoRequest') {
2507
+ return { message: { version: 2 } };
2508
+ }
2509
+ if (request === 'DeviceSessionAskPassphrase') {
2510
+ return { message: {} };
2511
+ }
2512
+ if (request === 'DeviceSessionGet') {
2513
+ return {
2514
+ message: {
2515
+ btc_test_address: 'hidden-state',
2516
+ session_id: 'hidden-session',
2517
+ seed_domains: STANDARD_SEED_DOMAINS,
2518
+ },
2519
+ };
2520
+ }
2521
+ throw new Error(`Unexpected request: ${request}`);
2522
+ });
2523
+ const device = createDevice({
2524
+ typedCall,
2525
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2526
+ });
2527
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'cached-session');
2528
+
2529
+ await expect(
2530
+ getProtocolV2WalletSession(device as any, {
2531
+ expectedPassphraseState: 'hidden-state',
2532
+ deriveCardano: true,
2533
+ })
2534
+ ).rejects.toMatchObject({
2535
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
2536
+ });
2537
+ expect(device.clearInternalState).toHaveBeenCalled();
2538
+ });
2539
+
2540
+ test('locks before selecting a passphrase wallet from an Attach PIN session', async () => {
2541
+ const typedCall = jest.fn((request: string) => {
2542
+ if (request === 'ProtocolInfoRequest') {
2543
+ return { message: { version: 2 } };
2544
+ }
2545
+ if (request === 'DeviceStatusGet') {
2546
+ return {
2547
+ message: {
2548
+ device_id: 'device-1',
2549
+ unlocked: true,
2550
+ passphrase_enabled: true,
2551
+ unlocked_by_attach_to_pin: true,
2552
+ },
2553
+ };
2554
+ }
2555
+ throw new Error(`Unexpected request: ${request}`);
2556
+ });
2557
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' });
2558
+ const device = createDevice({
2559
+ typedCall,
2560
+ promptPassphrase,
2561
+ unlockedAttachPin: true,
2562
+ });
2563
+
2564
+ await expect(
2565
+ getProtocolV2WalletSession(device as any, { forceWalletSelection: true })
2566
+ ).rejects.toMatchObject({
2567
+ errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
2568
+ });
2569
+ expect(device.lockDevice).toHaveBeenCalled();
2570
+ expect(promptPassphrase).not.toHaveBeenCalled();
2571
+ expect(typedCall).not.toHaveBeenCalledWith(
2572
+ 'DeviceSessionAskPassphrase',
2573
+ 'Success',
2574
+ expect.anything()
2575
+ );
2576
+ });
2577
+
2578
+ test('asks Standard-only seed domains when deriveCardano is false', async () => {
2579
+ const typedCall = jest.fn((request: string) => {
2580
+ if (request === 'ProtocolInfoRequest') {
2581
+ return { message: { version: 2 } };
2582
+ }
2583
+ if (request === 'DeviceSessionAskPassphrase') {
2584
+ return { message: {} };
2585
+ }
2586
+ if (request === 'DeviceSessionGet') {
2587
+ return {
2588
+ message: {
2589
+ btc_test_address: 'hidden-state',
2590
+ session_id: 'new-session',
2591
+ },
2592
+ };
2593
+ }
2594
+ throw new Error(`Unexpected request: ${request}`);
2595
+ });
2596
+ const device = createDevice({
2597
+ typedCall,
2598
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2599
+ });
2600
+
2601
+ await getProtocolV2WalletSession(device as any, {
2602
+ forceWalletSelection: true,
2603
+ deriveCardano: false,
2604
+ });
2605
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2606
+ passphrase: 'host hidden wallet',
2607
+ on_device: false,
2608
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2609
+ });
2610
+ });
1899
2611
  });