@onekeyfe/hd-core 1.2.2-alpha.1 → 1.2.2-alpha.10

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 (34) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +67 -0
  3. package/__tests__/open-wallet-session.test.ts +613 -80
  4. package/__tests__/pro2HostAssetPackage.test.ts +108 -1
  5. package/__tests__/protocol-v2.test.ts +194 -49
  6. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  7. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  8. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  9. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  10. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  11. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  12. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  13. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  14. package/dist/core/RequestQueue.d.ts +2 -0
  15. package/dist/core/RequestQueue.d.ts.map +1 -1
  16. package/dist/core/index.d.ts +2 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +1411 -1117
  20. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  21. package/dist/utils/patch.d.ts +1 -1
  22. package/dist/utils/patch.d.ts.map +1 -1
  23. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
  24. package/package.json +4 -4
  25. package/src/api/FirmwareUpdateV4.ts +42 -10
  26. package/src/api/OpenWalletSession.ts +0 -3
  27. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
  28. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  29. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +8 -2
  30. package/src/core/RequestQueue.ts +16 -1
  31. package/src/core/index.ts +48 -21
  32. package/src/data/messages/messages-protocol-v2.json +1471 -1367
  33. package/src/protocols/protocol-v2/walletSession.ts +157 -38
  34. package/src/utils/pro2HostAssetPackage.ts +73 -27
@@ -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('selects the Main PIN before opening the standard wallet 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,19 +179,113 @@ describe('openWalletSession', () => {
167
179
 
168
180
  await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
169
181
 
170
- expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
171
- source: 'wallet-session-coordinator',
172
- reason: 'open-wallet',
173
- deviceOnly: true,
174
- });
182
+ expect(device.unlockDevice).not.toHaveBeenCalled();
175
183
  expect(typedCall).not.toHaveBeenCalledWith(
176
184
  'DeviceSessionAskPassphrase',
177
185
  'Success',
178
186
  expect.anything()
179
187
  );
180
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
181
- 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',
182
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);
183
289
  });
184
290
 
185
291
  test('reuses a Main PIN selected by the current preflight when passphrase is disabled', async () => {
@@ -205,9 +311,7 @@ describe('openWalletSession', () => {
205
311
  });
206
312
 
207
313
  expect(device.unlockDevice).not.toHaveBeenCalled();
208
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
209
- seed_domains: [],
210
- });
314
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
211
315
  });
212
316
 
213
317
  test('does not treat an Attach PIN DeviceStatus as the Main wallet', async () => {
@@ -228,8 +332,7 @@ describe('openWalletSession', () => {
228
332
  }
229
333
  throw new Error(`Unexpected request: ${request}`);
230
334
  });
231
- const device = createDevice({ typedCall });
232
- device.features.unlockedAttachPin = true;
335
+ const device = createDevice({ refreshedUnlockedAttachPin: true, typedCall });
233
336
 
234
337
  await getProtocolV2WalletSession(device as any, { onlyMainPin: true });
235
338
 
@@ -334,10 +437,14 @@ describe('openWalletSession', () => {
334
437
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
335
438
  passphrase: 'host hidden wallet',
336
439
  on_device: false,
440
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
337
441
  });
338
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
339
- seed_domains: [],
340
- });
442
+ expect(typedCall).toHaveBeenNthCalledWith(
443
+ 3,
444
+ 'DeviceSessionGet',
445
+ 'DeviceSession',
446
+ standardSessionGet
447
+ );
341
448
  });
342
449
 
343
450
  test('keeps Legacy Protocol V1 getPassphraseState parameterless', async () => {
@@ -454,10 +561,14 @@ describe('openWalletSession', () => {
454
561
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
455
562
  passphrase: 'host hidden wallet',
456
563
  on_device: false,
564
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
457
565
  });
458
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
459
- seed_domains: [],
460
- });
566
+ expect(typedCall).toHaveBeenNthCalledWith(
567
+ 3,
568
+ 'DeviceSessionGet',
569
+ 'DeviceSession',
570
+ standardSessionGet
571
+ );
461
572
  });
462
573
 
463
574
  test('refreshes Pro2 status and unlocks before selecting a hidden wallet when locked', async () => {
@@ -731,9 +842,11 @@ describe('openWalletSession', () => {
731
842
  });
732
843
  expect(promptPassphrase).not.toHaveBeenCalled();
733
844
  expect(device.unlockDevice).not.toHaveBeenCalled();
734
- expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
735
- seed_domains: [],
736
- });
845
+ expect(device.commands.typedCall).toHaveBeenCalledWith(
846
+ 'DeviceSessionGet',
847
+ 'DeviceSession',
848
+ standardSessionGet
849
+ );
737
850
  expect(device.commands.typedCall).not.toHaveBeenCalledWith(
738
851
  'DeviceSessionAskPassphrase',
739
852
  'Success',
@@ -829,10 +942,9 @@ describe('openWalletSession', () => {
829
942
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
830
943
  passphrase: 'host hidden wallet',
831
944
  on_device: false,
945
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
832
946
  });
833
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
834
- seed_domains: [],
835
- });
947
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
836
948
  expect(promptPassphrase).toHaveBeenCalled();
837
949
  expect(deviceWalletSessionStore.get('device-1', 'new-hidden-state')).toBe('new-hidden-session');
838
950
  });
@@ -904,7 +1016,7 @@ describe('openWalletSession', () => {
904
1016
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
905
1017
  session_id: 'known-session',
906
1018
  btc_test_address: 'hidden-state',
907
- seed_domains: [],
1019
+ ...standardSessionGet,
908
1020
  });
909
1021
  expect(promptPassphrase).not.toHaveBeenCalled();
910
1022
  });
@@ -1087,10 +1199,9 @@ describe('openWalletSession', () => {
1087
1199
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1088
1200
  passphrase: '',
1089
1201
  on_device: false,
1202
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1090
1203
  });
1091
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1092
- seed_domains: [],
1093
- });
1204
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1094
1205
  expect(promptPassphrase).not.toHaveBeenCalled();
1095
1206
  expect(device.passphraseState).toBeUndefined();
1096
1207
  });
@@ -1110,7 +1221,7 @@ describe('openWalletSession', () => {
1110
1221
  payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'standard' },
1111
1222
  });
1112
1223
  method.init();
1113
- const device = createDevice({ typedCall });
1224
+ const device = createDevice({ refreshedUnlocked: false, typedCall });
1114
1225
  device.features.unlocked = false;
1115
1226
  device.getDeviceState = jest
1116
1227
  .fn()
@@ -1210,6 +1321,7 @@ describe('openWalletSession', () => {
1210
1321
  });
1211
1322
 
1212
1323
  test('switches from Attach PIN to Main PIN before opening the standard wallet', async () => {
1324
+ let attachPinSelected = true;
1213
1325
  const typedCall = jest
1214
1326
  .fn()
1215
1327
  .mockResolvedValueOnce({ message: { version: 2 } })
@@ -1226,6 +1338,21 @@ describe('openWalletSession', () => {
1226
1338
  method.init();
1227
1339
  const device = createDevice({ typedCall });
1228
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
+ });
1229
1356
  device.getDeviceState = jest
1230
1357
  .fn()
1231
1358
  .mockResolvedValueOnce({
@@ -1245,6 +1372,7 @@ describe('openWalletSession', () => {
1245
1372
  },
1246
1373
  });
1247
1374
  device.unlockDevice = jest.fn().mockImplementation(() => {
1375
+ attachPinSelected = false;
1248
1376
  device.features.unlockedAttachPin = false;
1249
1377
  return Promise.resolve(device.features);
1250
1378
  });
@@ -1259,9 +1387,7 @@ describe('openWalletSession', () => {
1259
1387
  reason: 'open-wallet',
1260
1388
  deviceOnly: true,
1261
1389
  });
1262
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1263
- seed_domains: [],
1264
- });
1390
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1265
1391
  });
1266
1392
 
1267
1393
  test('selects a hidden wallet without exposing the internal device session', async () => {
@@ -1294,10 +1420,9 @@ describe('openWalletSession', () => {
1294
1420
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1295
1421
  passphrase: 'host hidden wallet',
1296
1422
  on_device: false,
1423
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1297
1424
  });
1298
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1299
- seed_domains: [],
1300
- });
1425
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1301
1426
  expect(promptPassphrase).toHaveBeenCalled();
1302
1427
  expect(device.passphraseState).toBeUndefined();
1303
1428
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('hidden-session');
@@ -1356,10 +1481,9 @@ describe('openWalletSession', () => {
1356
1481
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
1357
1482
  passphrase: 'host hidden wallet',
1358
1483
  on_device: false,
1484
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1359
1485
  });
1360
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1361
- seed_domains: [],
1362
- });
1486
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1363
1487
  });
1364
1488
 
1365
1489
  test('selects an on-device passphrase wallet with an explicit on_device request', async () => {
@@ -1404,10 +1528,14 @@ describe('openWalletSession', () => {
1404
1528
  });
1405
1529
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1406
1530
  on_device: true,
1531
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1407
1532
  });
1408
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
1409
- seed_domains: [],
1410
- });
1533
+ expect(typedCall).toHaveBeenNthCalledWith(
1534
+ 3,
1535
+ 'DeviceSessionGet',
1536
+ 'DeviceSession',
1537
+ standardSessionGet
1538
+ );
1411
1539
  expect(device.createProtocolV2UiPhaseMetadata).toHaveBeenNthCalledWith(
1412
1540
  2,
1413
1541
  'passphrase-on-device',
@@ -1458,10 +1586,14 @@ describe('openWalletSession', () => {
1458
1586
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1459
1587
  passphrase: 'host hidden wallet',
1460
1588
  on_device: false,
1589
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1461
1590
  });
1462
- expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceSessionGet', 'DeviceSession', {
1463
- seed_domains: [],
1464
- });
1591
+ expect(typedCall).toHaveBeenNthCalledWith(
1592
+ 3,
1593
+ 'DeviceSessionGet',
1594
+ 'DeviceSession',
1595
+ standardSessionGet
1596
+ );
1465
1597
  });
1466
1598
 
1467
1599
  test('normalizes a Unicode Host passphrase before sending it to Pro2 firmware', async () => {
@@ -1483,6 +1615,7 @@ describe('openWalletSession', () => {
1483
1615
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1484
1616
  passphrase: 'cafe\u0301',
1485
1617
  on_device: false,
1618
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1486
1619
  });
1487
1620
  });
1488
1621
 
@@ -1506,6 +1639,7 @@ describe('openWalletSession', () => {
1506
1639
  expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
1507
1640
  passphrase,
1508
1641
  on_device: false,
1642
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1509
1643
  });
1510
1644
  });
1511
1645
 
@@ -1541,9 +1675,7 @@ describe('openWalletSession', () => {
1541
1675
  expect(device.unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin, {
1542
1676
  emitUiEvent: false,
1543
1677
  });
1544
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1545
- seed_domains: [],
1546
- });
1678
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
1547
1679
  });
1548
1680
 
1549
1681
  test('uses the complete Attach PIN wire flow without a main PIN unlock', async () => {
@@ -1572,7 +1704,7 @@ describe('openWalletSession', () => {
1572
1704
  ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1573
1705
  ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.AttachToPin }],
1574
1706
  ['DeviceStatusGet', 'DeviceStatus', {}],
1575
- ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
1707
+ ['DeviceSessionGet', 'DeviceSession', standardSessionGet],
1576
1708
  ]);
1577
1709
  expect(device.commands.typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
1578
1710
  type: DeviceSessionPinType.Main,
@@ -1784,7 +1916,7 @@ describe('openWalletSession', () => {
1784
1916
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1785
1917
  session_id: 'known-session',
1786
1918
  btc_test_address: 'hidden-state',
1787
- seed_domains: [],
1919
+ ...standardSessionGet,
1788
1920
  });
1789
1921
  expect(promptPassphrase).not.toHaveBeenCalled();
1790
1922
  });
@@ -1844,7 +1976,7 @@ describe('openWalletSession', () => {
1844
1976
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1845
1977
  session_id: 'known-session',
1846
1978
  btc_test_address: 'hidden-state',
1847
- seed_domains: [],
1979
+ ...standardSessionGet,
1848
1980
  });
1849
1981
  const sessionGetCall = typedCall.mock.calls.findIndex(
1850
1982
  ([requestName]) => requestName === 'DeviceSessionGet'
@@ -1942,7 +2074,7 @@ describe('openWalletSession', () => {
1942
2074
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1943
2075
  session_id: 'expired-session',
1944
2076
  btc_test_address: 'hidden-state',
1945
- seed_domains: [],
2077
+ ...standardSessionGet,
1946
2078
  });
1947
2079
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
1948
2080
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
@@ -1980,46 +2112,447 @@ describe('openWalletSession', () => {
1980
2112
  });
1981
2113
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1982
2114
  btc_test_address: 'hidden-state',
1983
- seed_domains: [],
2115
+ ...standardSessionGet,
1984
2116
  });
1985
2117
  expect(promptPassphrase).not.toHaveBeenCalled();
1986
2118
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
1987
2119
  });
1988
2120
 
1989
- test.each([false, true])('keeps deriveCardano API compatibility: %s', async deriveCardano => {
1990
- const typedCall = jest
1991
- .fn()
1992
- .mockResolvedValueOnce({ message: { version: 2 } })
1993
- .mockResolvedValueOnce({ message: {} })
1994
- .mockResolvedValueOnce({
1995
- message: {
1996
- btc_test_address: 'hidden-state',
1997
- session_id: 'new-session',
2121
+ test.each([false, true])(
2122
+ 'openWalletSession does not thread CommonParams.deriveCardano: %s',
2123
+ async deriveCardano => {
2124
+ const typedCall = jest
2125
+ .fn()
2126
+ .mockResolvedValueOnce({ message: { version: 2 } })
2127
+ .mockResolvedValueOnce({ message: {} })
2128
+ .mockResolvedValueOnce({
2129
+ message: {
2130
+ btc_test_address: 'hidden-state',
2131
+ session_id: 'new-session',
2132
+ },
2133
+ });
2134
+ const method = new OpenWalletSession({
2135
+ payload: {
2136
+ method: 'openWalletSession',
2137
+ connectId: 'connect-id',
2138
+ mode: 'select-hidden',
2139
+ deriveCardano,
1998
2140
  },
1999
2141
  });
2000
- const method = new OpenWalletSession({
2001
- payload: {
2002
- method: 'openWalletSession',
2003
- connectId: 'connect-id',
2004
- mode: 'select-hidden',
2005
- deriveCardano,
2006
- },
2142
+ method.init();
2143
+ method.device = createDevice({
2144
+ typedCall,
2145
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2146
+ }) as any;
2147
+
2148
+ await expect(method.run()).resolves.toMatchObject({
2149
+ walletType: 'hidden',
2150
+ passphraseState: 'hidden-state',
2151
+ });
2152
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2153
+ passphrase: 'host hidden wallet',
2154
+ on_device: false,
2155
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2156
+ });
2157
+ expect(typedCall).toHaveBeenCalledWith(
2158
+ 'DeviceSessionGet',
2159
+ 'DeviceSession',
2160
+ standardSessionGet
2161
+ );
2162
+ }
2163
+ );
2164
+
2165
+ test('requests Cardano seed domains when a V2 session rebuild has Cardano intent', async () => {
2166
+ const typedCall = jest.fn((request: string) => {
2167
+ if (request === 'ProtocolInfoRequest') {
2168
+ return { message: { version: 2 } };
2169
+ }
2170
+ if (request === 'DeviceSessionAskPassphrase') {
2171
+ return { message: {} };
2172
+ }
2173
+ if (request === 'DeviceSessionGet') {
2174
+ return {
2175
+ message: {
2176
+ btc_test_address: 'hidden-state',
2177
+ session_id: 'new-session',
2178
+ seed_domains: [
2179
+ DeviceSessionSeedDomain.SeedDomain_Standard,
2180
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
2181
+ ],
2182
+ },
2183
+ };
2184
+ }
2185
+ throw new Error(`Unexpected request: ${request}`);
2007
2186
  });
2008
- method.init();
2009
- method.device = createDevice({
2187
+ const device = createDevice({
2010
2188
  typedCall,
2011
2189
  promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2012
- }) as any;
2190
+ });
2013
2191
 
2014
- await expect(method.run()).resolves.toMatchObject({
2015
- walletType: 'hidden',
2192
+ await expect(
2193
+ getProtocolV2WalletSession(device as any, {
2194
+ forceWalletSelection: true,
2195
+ deriveCardano: true,
2196
+ })
2197
+ ).resolves.toMatchObject({
2016
2198
  passphraseState: 'hidden-state',
2199
+ newSession: 'new-session',
2017
2200
  });
2201
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2202
+ passphrase: 'host hidden wallet',
2203
+ on_device: false,
2204
+ seed_domains: CARDANO_SEED_DOMAINS,
2205
+ });
2206
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', standardSessionGet);
2207
+ });
2208
+
2209
+ test('asks Cardano seed domains after Get reports a Standard-only session', async () => {
2210
+ const typedCall = jest.fn((request: string) => {
2211
+ if (request === 'ProtocolInfoRequest') {
2212
+ return { message: { version: 2 } };
2213
+ }
2214
+ if (request === 'DeviceSessionAskPassphrase') {
2215
+ return { message: {} };
2216
+ }
2217
+ if (request === 'DeviceSessionGet') {
2218
+ const hasCardanoAsk = typedCall.mock.calls.some(
2219
+ call =>
2220
+ call[0] === 'DeviceSessionAskPassphrase' &&
2221
+ Array.isArray(call[2]?.seed_domains) &&
2222
+ call[2].seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano)
2223
+ );
2224
+ return {
2225
+ message: {
2226
+ btc_test_address: 'hidden-state',
2227
+ session_id: 'new-session',
2228
+ seed_domains: hasCardanoAsk
2229
+ ? [
2230
+ DeviceSessionSeedDomain.SeedDomain_Standard,
2231
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
2232
+ ]
2233
+ : [DeviceSessionSeedDomain.SeedDomain_Standard],
2234
+ },
2235
+ };
2236
+ }
2237
+ throw new Error(`Unexpected request: ${request}`);
2238
+ });
2239
+ const device = createDevice({
2240
+ typedCall,
2241
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2242
+ });
2243
+ device.passphraseState = 'hidden-state';
2244
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'cached-session');
2245
+
2246
+ await getProtocolV2WalletSession(device as any, {
2247
+ expectedPassphraseState: 'hidden-state',
2248
+ deriveCardano: true,
2249
+ });
2250
+
2018
2251
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2252
+ session_id: 'cached-session',
2253
+ btc_test_address: 'hidden-state',
2254
+ });
2255
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2256
+ passphrase: 'host hidden wallet',
2257
+ on_device: false,
2019
2258
  seed_domains: [
2020
2259
  DeviceSessionSeedDomain.SeedDomain_Standard,
2021
- ...(deriveCardano ? [DeviceSessionSeedDomain.SeedDomain_Cardano] : []),
2260
+ DeviceSessionSeedDomain.SeedDomain_Cardano,
2022
2261
  ],
2023
2262
  });
2024
2263
  });
2264
+
2265
+ test('uses empty AskPassphrase to add Cardano on an Attach PIN session', async () => {
2266
+ const typedCall = jest.fn((request: string) => {
2267
+ if (request === 'ProtocolInfoRequest') {
2268
+ return { message: { version: 2 } };
2269
+ }
2270
+ if (request === 'DeviceSessionAskPassphrase') {
2271
+ return { message: {} };
2272
+ }
2273
+ if (request === 'DeviceSessionGet') {
2274
+ const askedCardano = typedCall.mock.calls.some(
2275
+ call =>
2276
+ call[0] === 'DeviceSessionAskPassphrase' &&
2277
+ Array.isArray(call[2]?.seed_domains) &&
2278
+ call[2].seed_domains.includes(DeviceSessionSeedDomain.SeedDomain_Cardano)
2279
+ );
2280
+ return {
2281
+ message: {
2282
+ btc_test_address: 'attach-state',
2283
+ session_id: 'attach-session',
2284
+ seed_domains: askedCardano ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS,
2285
+ },
2286
+ };
2287
+ }
2288
+ throw new Error(`Unexpected request: ${request}`);
2289
+ });
2290
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' });
2291
+ const device = createDevice({
2292
+ typedCall,
2293
+ promptPassphrase,
2294
+ unlockedAttachPin: true,
2295
+ });
2296
+
2297
+ await expect(
2298
+ getProtocolV2WalletSession(device as any, {
2299
+ readCurrentAttachPinSession: true,
2300
+ deriveCardano: true,
2301
+ })
2302
+ ).resolves.toMatchObject({
2303
+ passphraseState: 'attach-state',
2304
+ newSession: 'attach-session',
2305
+ unlockedAttachPin: true,
2306
+ });
2307
+ expect(promptPassphrase).not.toHaveBeenCalled();
2308
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2309
+ passphrase: '',
2310
+ on_device: false,
2311
+ seed_domains: CARDANO_SEED_DOMAINS,
2312
+ });
2313
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2314
+ expect(
2315
+ typedCall.mock.calls
2316
+ .filter(call => call[0] === 'DeviceSessionGet')
2317
+ .every(call => !('seed_domains' in (call[2] ?? {})))
2318
+ ).toBe(true);
2319
+ });
2320
+
2321
+ test('uses empty AskPassphrase after selecting Attach PIN for Cardano intent', async () => {
2322
+ const typedCall = jest.fn((request: string) => {
2323
+ if (request === 'ProtocolInfoRequest') {
2324
+ return { message: { version: 2 } };
2325
+ }
2326
+ if (request === 'DeviceSessionAskPassphrase') {
2327
+ return { message: {} };
2328
+ }
2329
+ if (request === 'DeviceSessionGet') {
2330
+ return {
2331
+ message: {
2332
+ btc_test_address: 'attach-state',
2333
+ session_id: 'attach-session',
2334
+ seed_domains: CARDANO_SEED_DOMAINS,
2335
+ },
2336
+ };
2337
+ }
2338
+ throw new Error(`Unexpected request: ${request}`);
2339
+ });
2340
+ const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
2341
+ const device = createDevice({ typedCall, promptPassphrase });
2342
+ device.features.attachToPinEnabled = true;
2343
+
2344
+ await expect(
2345
+ getProtocolV2WalletSession(device as any, {
2346
+ forceWalletSelection: true,
2347
+ deriveCardano: true,
2348
+ })
2349
+ ).resolves.toMatchObject({
2350
+ passphraseState: 'attach-state',
2351
+ newSession: 'attach-session',
2352
+ unlockedAttachPin: true,
2353
+ });
2354
+ expect(promptPassphrase).toHaveBeenCalledTimes(1);
2355
+ expect(device.unlockDevice).toHaveBeenCalledWith(
2356
+ DeviceSessionPinType.AttachToPin,
2357
+ expect.objectContaining({ emitUiEvent: false })
2358
+ );
2359
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2360
+ passphrase: '',
2361
+ on_device: false,
2362
+ seed_domains: CARDANO_SEED_DOMAINS,
2363
+ });
2364
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2365
+ expect(
2366
+ typedCall.mock.calls
2367
+ .filter(call => call[0] === 'DeviceSessionGet')
2368
+ .every(call => !('seed_domains' in (call[2] ?? {})))
2369
+ ).toBe(true);
2370
+ });
2371
+
2372
+ test('still locks before a passphrase picker after Attach PIN Cardano empty Ask', async () => {
2373
+ const typedCall = jest.fn((request: string) => {
2374
+ if (request === 'ProtocolInfoRequest') {
2375
+ return { message: { version: 2 } };
2376
+ }
2377
+ if (request === 'DeviceSessionAskPassphrase') {
2378
+ return { message: {} };
2379
+ }
2380
+ if (request === 'DeviceSessionGet') {
2381
+ return {
2382
+ message: {
2383
+ btc_test_address: 'attach-state',
2384
+ session_id: 'attach-session',
2385
+ seed_domains: CARDANO_SEED_DOMAINS,
2386
+ },
2387
+ };
2388
+ }
2389
+ throw new Error(`Unexpected request: ${request}`);
2390
+ });
2391
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' });
2392
+ const device = createDevice({
2393
+ typedCall,
2394
+ promptPassphrase,
2395
+ unlockedAttachPin: true,
2396
+ });
2397
+
2398
+ await getProtocolV2WalletSession(device as any, {
2399
+ readCurrentAttachPinSession: true,
2400
+ deriveCardano: true,
2401
+ });
2402
+ await expect(
2403
+ getProtocolV2WalletSession(device as any, { forceWalletSelection: true })
2404
+ ).rejects.toMatchObject({
2405
+ errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
2406
+ });
2407
+ expect(device.lockDevice).toHaveBeenCalled();
2408
+ expect(promptPassphrase).not.toHaveBeenCalled();
2409
+ });
2410
+
2411
+ test('uses a second Get for Cardano when passphrase protection is off', async () => {
2412
+ const typedCall = jest.fn((request: string) => {
2413
+ if (request === 'ProtocolInfoRequest') {
2414
+ return { message: { version: 2 } };
2415
+ }
2416
+ if (request === 'DeviceSessionGet') {
2417
+ const getCount = typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet').length;
2418
+ return {
2419
+ message: {
2420
+ btc_test_address: 'standard-state',
2421
+ session_id: 'standard-session',
2422
+ seed_domains: getCount > 1 ? CARDANO_SEED_DOMAINS : STANDARD_SEED_DOMAINS,
2423
+ },
2424
+ };
2425
+ }
2426
+ throw new Error(`Unexpected request: ${request}`);
2427
+ });
2428
+ const device = createDevice({
2429
+ typedCall,
2430
+ passphraseProtection: false,
2431
+ });
2432
+
2433
+ await expect(
2434
+ getProtocolV2WalletSession(device as any, {
2435
+ onlyMainPin: true,
2436
+ deriveCardano: true,
2437
+ })
2438
+ ).resolves.toMatchObject({
2439
+ passphraseState: 'standard-state',
2440
+ newSession: 'standard-session',
2441
+ });
2442
+ expect(typedCall).not.toHaveBeenCalledWith(
2443
+ 'DeviceSessionAskPassphrase',
2444
+ 'Success',
2445
+ expect.anything()
2446
+ );
2447
+ expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(2);
2448
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
2449
+ });
2450
+
2451
+ test('fails closed when Cardano fallback still lacks Cardano', async () => {
2452
+ const typedCall = jest.fn((request: string) => {
2453
+ if (request === 'ProtocolInfoRequest') {
2454
+ return { message: { version: 2 } };
2455
+ }
2456
+ if (request === 'DeviceSessionAskPassphrase') {
2457
+ return { message: {} };
2458
+ }
2459
+ if (request === 'DeviceSessionGet') {
2460
+ return {
2461
+ message: {
2462
+ btc_test_address: 'hidden-state',
2463
+ session_id: 'hidden-session',
2464
+ seed_domains: STANDARD_SEED_DOMAINS,
2465
+ },
2466
+ };
2467
+ }
2468
+ throw new Error(`Unexpected request: ${request}`);
2469
+ });
2470
+ const device = createDevice({
2471
+ typedCall,
2472
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2473
+ });
2474
+ deviceWalletSessionStore.set('device-1', 'hidden-state', 'cached-session');
2475
+
2476
+ await expect(
2477
+ getProtocolV2WalletSession(device as any, {
2478
+ expectedPassphraseState: 'hidden-state',
2479
+ deriveCardano: true,
2480
+ })
2481
+ ).rejects.toMatchObject({
2482
+ errorCode: HardwareErrorCode.WalletSessionInvalid,
2483
+ });
2484
+ expect(device.clearInternalState).toHaveBeenCalled();
2485
+ });
2486
+
2487
+ test('locks before selecting a passphrase wallet from an Attach PIN session', async () => {
2488
+ const typedCall = jest.fn((request: string) => {
2489
+ if (request === 'ProtocolInfoRequest') {
2490
+ return { message: { version: 2 } };
2491
+ }
2492
+ if (request === 'DeviceStatusGet') {
2493
+ return {
2494
+ message: {
2495
+ device_id: 'device-1',
2496
+ unlocked: true,
2497
+ passphrase_enabled: true,
2498
+ unlocked_by_attach_to_pin: true,
2499
+ },
2500
+ };
2501
+ }
2502
+ throw new Error(`Unexpected request: ${request}`);
2503
+ });
2504
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'should-not-ask' });
2505
+ const device = createDevice({
2506
+ typedCall,
2507
+ promptPassphrase,
2508
+ unlockedAttachPin: true,
2509
+ });
2510
+
2511
+ await expect(
2512
+ getProtocolV2WalletSession(device as any, { forceWalletSelection: true })
2513
+ ).rejects.toMatchObject({
2514
+ errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
2515
+ });
2516
+ expect(device.lockDevice).toHaveBeenCalled();
2517
+ expect(promptPassphrase).not.toHaveBeenCalled();
2518
+ expect(typedCall).not.toHaveBeenCalledWith(
2519
+ 'DeviceSessionAskPassphrase',
2520
+ 'Success',
2521
+ expect.anything()
2522
+ );
2523
+ });
2524
+
2525
+ test('asks Standard-only seed domains when deriveCardano is false', async () => {
2526
+ const typedCall = jest.fn((request: string) => {
2527
+ if (request === 'ProtocolInfoRequest') {
2528
+ return { message: { version: 2 } };
2529
+ }
2530
+ if (request === 'DeviceSessionAskPassphrase') {
2531
+ return { message: {} };
2532
+ }
2533
+ if (request === 'DeviceSessionGet') {
2534
+ return {
2535
+ message: {
2536
+ btc_test_address: 'hidden-state',
2537
+ session_id: 'new-session',
2538
+ },
2539
+ };
2540
+ }
2541
+ throw new Error(`Unexpected request: ${request}`);
2542
+ });
2543
+ const device = createDevice({
2544
+ typedCall,
2545
+ promptPassphrase: jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' }),
2546
+ });
2547
+
2548
+ await getProtocolV2WalletSession(device as any, {
2549
+ forceWalletSelection: true,
2550
+ deriveCardano: false,
2551
+ });
2552
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2553
+ passphrase: 'host hidden wallet',
2554
+ on_device: false,
2555
+ seed_domains: [DeviceSessionSeedDomain.SeedDomain_Standard],
2556
+ });
2557
+ });
2025
2558
  });