@onekeyfe/hd-core 1.2.0-alpha.36 → 1.2.0-alpha.39

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 +5 -1
  2. package/__tests__/deviceUploadNft.test.ts +187 -0
  3. package/__tests__/homescreen.test.ts +41 -1
  4. package/__tests__/logBlockEvent.test.ts +1 -1
  5. package/__tests__/open-wallet-session.test.ts +161 -44
  6. package/__tests__/pro2Nft.test.ts +108 -0
  7. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  8. package/__tests__/protocol-v2.test.ts +168 -444
  9. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  10. package/__tests__/uiProgressThrottle.test.ts +74 -0
  11. package/dist/api/BaseMethod.d.ts +3 -1
  12. package/dist/api/BaseMethod.d.ts.map +1 -1
  13. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  14. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -1
  16. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  17. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  18. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  19. package/dist/api/index.d.ts +1 -0
  20. package/dist/api/index.d.ts.map +1 -1
  21. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  22. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/events/logBlockEvent.d.ts.map +1 -1
  25. package/dist/index.d.ts +34 -2
  26. package/dist/index.js +544 -496
  27. package/dist/inject.d.ts.map +1 -1
  28. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  29. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  31. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  32. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  33. package/dist/types/api/index.d.ts +2 -1
  34. package/dist/types/api/index.d.ts.map +1 -1
  35. package/dist/types/api/protocolV2.d.ts +3 -0
  36. package/dist/types/api/protocolV2.d.ts.map +1 -1
  37. package/dist/utils/homescreen.d.ts +4 -0
  38. package/dist/utils/homescreen.d.ts.map +1 -1
  39. package/dist/utils/index.d.ts +1 -1
  40. package/dist/utils/index.d.ts.map +1 -1
  41. package/dist/utils/patch.d.ts +1 -1
  42. package/dist/utils/patch.d.ts.map +1 -1
  43. package/dist/utils/pro2Nft.d.ts +29 -0
  44. package/dist/utils/pro2Nft.d.ts.map +1 -0
  45. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  46. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  47. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  48. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  49. package/package.json +4 -4
  50. package/src/api/BaseMethod.ts +7 -9
  51. package/src/api/OpenWalletSession.ts +37 -15
  52. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +29 -30
  53. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -7
  54. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  55. package/src/api/index.ts +1 -0
  56. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  57. package/src/core/index.ts +83 -82
  58. package/src/data/messages/messages-protocol-v2.json +10 -15
  59. package/src/events/logBlockEvent.ts +1 -0
  60. package/src/inject.ts +1 -0
  61. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  62. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  63. package/src/protocols/protocol-v2/walletSession.ts +18 -51
  64. package/src/types/api/index.ts +2 -0
  65. package/src/types/api/protocolV2.ts +13 -0
  66. package/src/utils/homescreen.ts +32 -6
  67. package/src/utils/index.ts +6 -1
  68. package/src/utils/pro2Nft.ts +91 -0
  69. package/src/utils/pro2Wallpaper.ts +35 -8
  70. package/src/utils/uiProgressThrottle.ts +63 -0
  71. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  72. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  73. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  74. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  75. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  76. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  77. package/src/api/firmware/progressThrottle.ts +0 -44
  78. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  79. package/src/protocols/protocol-v2/unlockRetry.ts +0 -131
@@ -33,6 +33,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
33
33
  init: jest.fn(),
34
34
  name: 'evmGetAddress',
35
35
  responseID: 43,
36
+ unlockPolicy: 'unlock-before-run',
36
37
  run: jest.fn().mockImplementation(() => {
37
38
  calls.push('run-chain-method');
38
39
  return Promise.resolve([{ address: '0xhidden' }]);
@@ -51,10 +52,12 @@ describe('AllNetworkGetAddressBase tracing', () => {
51
52
  bundle: [],
52
53
  },
53
54
  });
55
+ method.protocolV2UnlockContext = { preflightCompleted: true };
56
+ const typedCall = jest.fn();
54
57
  method.device = {
55
58
  checkPassphraseStateSafety,
56
59
  commands: {
57
- typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
60
+ typedCall,
58
61
  },
59
62
  getCurrentFirmwareType: jest.fn(),
60
63
  getProtocol: jest.fn().mockReturnValue('V2'),
@@ -89,6 +92,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
89
92
 
90
93
  expect(checkPassphraseStateSafety).toHaveBeenCalledWith('hidden-state', false, undefined);
91
94
  expect(calls).toEqual(['resume-hidden-session', 'run-chain-method']);
95
+ expect(typedCall).not.toHaveBeenCalled();
92
96
  });
93
97
 
94
98
  test('releases the nested request context when an unhandled error escapes', async () => {
@@ -0,0 +1,187 @@
1
+ import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
2
+
3
+ jest.mock('../src/data/config', () => ({
4
+ getSDKVersion: jest.fn(() => '1.0.0'),
5
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
6
+ }));
7
+
8
+ const createRgba = (width: number, height: number) => {
9
+ const data = new Uint8Array(width * height * 4);
10
+ for (let index = 3; index < data.length; index += 4) data[index] = 0xff;
11
+ return data;
12
+ };
13
+
14
+ const createMethod = ({
15
+ typedCall,
16
+ supportedMessages = [60805, 61500],
17
+ useFullBundle = false,
18
+ }: {
19
+ typedCall: jest.Mock;
20
+ supportedMessages?: number[];
21
+ useFullBundle?: boolean;
22
+ }) => {
23
+ const method = new DeviceUploadNft({
24
+ id: 1,
25
+ payload: {
26
+ method: 'deviceUploadNft',
27
+ image: { width: 540, height: 540, rgba: createRgba(540, 540) },
28
+ thumbnail: { width: 263, height: 263, rgba: createRgba(263, 263) },
29
+ title: 'CryptoPunk #3100',
30
+ subtitle: 'CryptoPunks',
31
+ timestampMs: 1_760_000_000_000,
32
+ chunkSize: 2048,
33
+ paceMs: 0,
34
+ },
35
+ });
36
+ (method as any).device = {
37
+ commands: { typedCall },
38
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
39
+ Promise.resolve({
40
+ version: 2,
41
+ build_fingerprint: 'application__1.0.0__test__DEV__DEBUG',
42
+ supported_messages: supportedMessages,
43
+ })
44
+ ),
45
+ getCurrentFirmwareType: jest.fn(),
46
+ };
47
+ method.postMessage = jest.fn();
48
+
49
+ if (useFullBundle) {
50
+ method.init();
51
+ } else {
52
+ (method as any).params = { chunkSize: 2048, paceMs: 0, timeoutMs: 15_000 };
53
+ (method as any).bundle = {
54
+ basename: 'nft-deadbeef-1760000000000',
55
+ image: new Uint8Array([1]),
56
+ thumbnail: new Uint8Array([2]),
57
+ metadata: new TextEncoder().encode('{"title":"NFT","subtitle":""}'),
58
+ };
59
+ }
60
+
61
+ return method;
62
+ };
63
+
64
+ const fileWriteSuccess = (params: { file: { offset: number; data: Uint8Array } }) => ({
65
+ message: {
66
+ processed_byte: params.file.offset + params.file.data.byteLength,
67
+ },
68
+ });
69
+
70
+ describe('DeviceUploadNft', () => {
71
+ test('uploads the triplet in order without creating the firmware-owned directory', async () => {
72
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
73
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
74
+ if (request === 'NftUpdate') return { message: { message: 'NFT updated' } };
75
+ throw new Error(`Unexpected request: ${request}`);
76
+ });
77
+ const method = createMethod({ typedCall, useFullBundle: true });
78
+
79
+ const result = await method.run();
80
+
81
+ const requests = typedCall.mock.calls.map(call => call[0]);
82
+ expect(requests[0]).toBe('FilesystemFileWrite');
83
+ expect(requests).not.toContain('FilesystemDirMake');
84
+ expect(requests.at(-1)).toBe('NftUpdate');
85
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
86
+ const paths = fileWrites.map(call => call[2].file.path as string);
87
+ const imagePath = paths[0];
88
+ const thumbnailStart = paths.findIndex(path => path.endsWith('_m.bin'));
89
+ const metadataStart = paths.findIndex(path => path.endsWith('.json'));
90
+ expect(imagePath).toMatch(/^vol1:\/nft\/nft-[a-f0-9]{8}-1760000000000\.bin$/);
91
+ expect(thumbnailStart).toBeGreaterThan(0);
92
+ expect(metadataStart).toBeGreaterThan(thumbnailStart);
93
+ expect(new Set(paths.slice(0, thumbnailStart))).toEqual(new Set([imagePath]));
94
+ expect(new Set(paths.slice(thumbnailStart, metadataStart))).toEqual(
95
+ new Set([result.thumbnailPath])
96
+ );
97
+ expect(typedCall).toHaveBeenLastCalledWith(
98
+ 'NftUpdate',
99
+ 'Success',
100
+ { file_name_no_ext: result.basename },
101
+ { timeoutMs: 15_000 }
102
+ );
103
+ expect(result).toMatchObject({
104
+ nftUpdated: true,
105
+ message: 'NFT updated',
106
+ totalSize: 583_212 + 138_876 + 53,
107
+ });
108
+ expect(method.postMessage).toHaveBeenLastCalledWith({
109
+ event: 'UI_EVENT',
110
+ type: 'ui-device_progress',
111
+ payload: expect.objectContaining({
112
+ progress: 100,
113
+ transferredBytes: result.totalSize,
114
+ totalBytes: result.totalSize,
115
+ }),
116
+ });
117
+ });
118
+
119
+ test('retries one timed-out NftUpdate with the same basename', async () => {
120
+ const timeout = Object.assign(new Error('Protocol V2 response timeout'), {
121
+ code: 'response-timeout',
122
+ });
123
+ let updateCalls = 0;
124
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
125
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
126
+ if (request === 'NftUpdate') {
127
+ updateCalls += 1;
128
+ if (updateCalls === 1) throw timeout;
129
+ return { message: { message: 'NFT updated' } };
130
+ }
131
+ throw new Error(`Unexpected request: ${request}`);
132
+ });
133
+ const method = createMethod({ typedCall });
134
+
135
+ await expect(method.run()).resolves.toMatchObject({ nftUpdated: true });
136
+
137
+ const updateRequests = typedCall.mock.calls.filter(call => call[0] === 'NftUpdate');
138
+ expect(updateRequests).toHaveLength(2);
139
+ expect(updateRequests[1]).toEqual(updateRequests[0]);
140
+ });
141
+
142
+ test('does not retry a non-timeout NftUpdate failure', async () => {
143
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
144
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
145
+ if (request === 'NftUpdate') throw new Error('Invalid NFT metadata');
146
+ throw new Error(`Unexpected request: ${request}`);
147
+ });
148
+ const method = createMethod({ typedCall });
149
+
150
+ await expect(method.run()).rejects.toThrow('Invalid NFT metadata');
151
+ expect(typedCall.mock.calls.filter(call => call[0] === 'NftUpdate')).toHaveLength(1);
152
+ });
153
+
154
+ test('propagates a second NftUpdate timeout after one retry', async () => {
155
+ const timeout = Object.assign(new Error('Protocol V2 response timeout'), {
156
+ code: 'response-timeout',
157
+ });
158
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
159
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
160
+ if (request === 'NftUpdate') throw timeout;
161
+ throw new Error(`Unexpected request: ${request}`);
162
+ });
163
+ const method = createMethod({ typedCall });
164
+
165
+ await expect(method.run()).rejects.toBe(timeout);
166
+ expect(typedCall.mock.calls.filter(call => call[0] === 'NftUpdate')).toHaveLength(2);
167
+ });
168
+
169
+ test('does not publish when a file write fails', async () => {
170
+ const typedCall = jest.fn((request: string) => {
171
+ if (request === 'FilesystemFileWrite') throw new Error('Filesystem full');
172
+ throw new Error(`Unexpected request: ${request}`);
173
+ });
174
+ const method = createMethod({ typedCall });
175
+
176
+ await expect(method.run()).rejects.toThrow('Filesystem full');
177
+ expect(typedCall.mock.calls.some(call => call[0] === 'NftUpdate')).toBe(false);
178
+ });
179
+
180
+ test('rejects unsupported firmware before writing files', async () => {
181
+ const typedCall = jest.fn();
182
+ const method = createMethod({ typedCall, supportedMessages: [60805] });
183
+
184
+ await expect(method.run()).rejects.toBeDefined();
185
+ expect(typedCall).not.toHaveBeenCalled();
186
+ });
187
+ });
@@ -1,6 +1,6 @@
1
1
  import { EDeviceType } from '@onekeyfe/hd-shared';
2
2
 
3
- import { getHomeScreenSize } from '../src/utils/homescreen';
3
+ import { getHomeScreenSize, getNftSize } from '../src/utils/homescreen';
4
4
 
5
5
  describe('getHomeScreenSize', () => {
6
6
  it('returns the Pro 2 wallpaper dimensions', () => {
@@ -21,4 +21,44 @@ describe('getHomeScreenSize', () => {
21
21
  })
22
22
  ).toBeUndefined();
23
23
  });
24
+
25
+ it('does not reuse the wallpaper homeScreenType for Pro 2 NFT dimensions', () => {
26
+ expect(
27
+ getHomeScreenSize({
28
+ deviceType: EDeviceType.Pro2,
29
+ homeScreenType: 'Nft',
30
+ thumbnail: false,
31
+ })
32
+ ).toEqual({ width: 604, height: 1024 });
33
+ expect(
34
+ getHomeScreenSize({
35
+ deviceType: EDeviceType.Pro2,
36
+ homeScreenType: 'Nft',
37
+ thumbnail: true,
38
+ })
39
+ ).toBeUndefined();
40
+ });
41
+ });
42
+
43
+ describe('getNftSize', () => {
44
+ it('returns the Pro 2 NFT image and thumbnail dimensions independently', () => {
45
+ expect(getNftSize({ deviceType: EDeviceType.Pro2 })).toEqual({ width: 540, height: 540 });
46
+ expect(getNftSize({ deviceType: EDeviceType.Pro2, thumbnail: true })).toEqual({
47
+ width: 263,
48
+ height: 263,
49
+ });
50
+ });
51
+
52
+ it('preserves the legacy NFT dimensions', () => {
53
+ expect(getNftSize({ deviceType: EDeviceType.Touch })).toEqual({ width: 480, height: 800 });
54
+ expect(getNftSize({ deviceType: EDeviceType.Touch, thumbnail: true })).toEqual({
55
+ width: 238,
56
+ height: 238,
57
+ });
58
+ expect(getNftSize({ deviceType: EDeviceType.Pro, thumbnail: true })).toEqual({
59
+ width: 226,
60
+ height: 226,
61
+ radius: 40,
62
+ });
63
+ });
24
64
  });
@@ -104,7 +104,7 @@ describe('getLogBlockLabel', () => {
104
104
  });
105
105
  });
106
106
 
107
- it.each(['deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
107
+ it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
108
108
  'keeps metadata and replaces binary payloads with their size for %s',
109
109
  method => {
110
110
  const request = { method, path: 'resource.bin', data: new Uint8Array(1024) };
@@ -1,5 +1,5 @@
1
1
  import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DeviceSessionPinType, DeviceSessionSeedDomain } from '@onekeyfe/hd-transport';
2
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import GetPassphraseState from '../src/api/GetPassphraseState';
5
5
  import OpenWalletSession from '../src/api/OpenWalletSession';
@@ -27,6 +27,7 @@ const createDevice = ({
27
27
  unlocked: true,
28
28
  passphraseProtection,
29
29
  attachToPinEnabled: false,
30
+ unlockedAttachPin: false,
30
31
  },
31
32
  commands: {
32
33
  typedCall: jest.fn((request: string, ...args: unknown[]) => {
@@ -49,10 +50,16 @@ const createDevice = ({
49
50
  eventless_wallet_session: true,
50
51
  })
51
52
  ),
52
- getDeviceState: jest.fn().mockResolvedValue({
53
- identity: { deviceId: 'device-1' },
54
- status: { passphraseProtection },
55
- }),
53
+ getDeviceState: jest.fn(() =>
54
+ Promise.resolve({
55
+ identity: { deviceId: 'device-1' },
56
+ status: {
57
+ unlocked: device.features.unlocked,
58
+ passphraseProtection: device.features.passphraseProtection,
59
+ unlockedAttachPin: device.features.unlockedAttachPin,
60
+ },
61
+ })
62
+ ),
56
63
  getCurrentDeviceId: () => 'device-1',
57
64
  getCurrentPassphraseProtection: () => passphraseProtection,
58
65
  getInternalState: () =>
@@ -323,10 +330,24 @@ describe('openWalletSession', () => {
323
330
  typedCall,
324
331
  promptPassphrase: jest.fn().mockResolvedValue({ passphraseOnDevice: true }),
325
332
  });
326
- device.getDeviceState = jest.fn().mockResolvedValue({
327
- identity: { deviceId: 'device-1' },
328
- status: { unlocked: false, passphraseProtection: true },
329
- });
333
+ device.getDeviceState = jest
334
+ .fn()
335
+ .mockResolvedValueOnce({
336
+ identity: { deviceId: 'device-1' },
337
+ status: {
338
+ unlocked: false,
339
+ passphraseProtection: null,
340
+ unlockedAttachPin: null,
341
+ },
342
+ })
343
+ .mockResolvedValue({
344
+ identity: { deviceId: 'device-1' },
345
+ status: {
346
+ unlocked: true,
347
+ passphraseProtection: true,
348
+ unlockedAttachPin: false,
349
+ },
350
+ });
330
351
  method.device = device as any;
331
352
 
332
353
  await expect(method.run()).resolves.toMatchObject({
@@ -380,6 +401,12 @@ describe('openWalletSession', () => {
380
401
  status.passphrase_enabled ?? device.features.passphraseProtection;
381
402
  return device.features;
382
403
  });
404
+ device.unlockDevice = jest.fn().mockImplementation(() => {
405
+ device.features.unlocked = true;
406
+ device.features.passphraseProtection = true;
407
+ device.features.unlockedAttachPin = false;
408
+ return Promise.resolve(device.features);
409
+ });
383
410
  method.device = device as any;
384
411
 
385
412
  await expect(method.run()).resolves.toEqual({
@@ -389,10 +416,98 @@ describe('openWalletSession', () => {
389
416
  passphraseState: 'hidden-state-after-unlock',
390
417
  resumed: false,
391
418
  });
392
- expect(device.unlockDevice).not.toHaveBeenCalled();
419
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
393
420
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
394
421
  });
395
422
 
423
+ test('unlocks before wallet selection when the Protocol V2 wallet status is unknown', async () => {
424
+ const typedCall = jest
425
+ .fn()
426
+ .mockResolvedValueOnce({ message: { version: 2 } })
427
+ .mockResolvedValueOnce({ message: {} })
428
+ .mockResolvedValueOnce({
429
+ message: {
430
+ btc_test_address: 'hidden-state-after-unknown-status',
431
+ session_id: 'hidden-session-after-unknown-status',
432
+ },
433
+ });
434
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
435
+ const method = new OpenWalletSession({
436
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
437
+ });
438
+ method.init();
439
+ const device = createDevice({ typedCall, promptPassphrase });
440
+ device.getDeviceState = jest
441
+ .fn()
442
+ .mockResolvedValueOnce({
443
+ identity: { deviceId: 'device-1' },
444
+ status: {
445
+ unlocked: undefined,
446
+ passphraseProtection: null,
447
+ unlockedAttachPin: null,
448
+ },
449
+ })
450
+ .mockResolvedValue({
451
+ identity: { deviceId: 'device-1' },
452
+ status: {
453
+ unlocked: true,
454
+ passphraseProtection: true,
455
+ unlockedAttachPin: false,
456
+ },
457
+ });
458
+ device.unlockDevice = jest.fn().mockImplementation(() => {
459
+ device.features.unlocked = true;
460
+ device.features.passphraseProtection = true;
461
+ device.features.unlockedAttachPin = false;
462
+ return Promise.resolve(device.features);
463
+ });
464
+ method.device = device as any;
465
+
466
+ await expect(method.run()).resolves.toMatchObject({
467
+ walletType: 'hidden',
468
+ passphraseState: 'hidden-state-after-unknown-status',
469
+ });
470
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
471
+ expect(device.unlockDevice.mock.invocationCallOrder[0]).toBeLessThan(
472
+ promptPassphrase.mock.invocationCallOrder[0]
473
+ );
474
+ });
475
+
476
+ test('rejects wallet selection when the post-unlock Protocol V2 status remains incomplete', async () => {
477
+ const typedCall = jest
478
+ .fn()
479
+ .mockResolvedValueOnce({ message: { version: 2 } })
480
+ .mockResolvedValueOnce({ message: {} })
481
+ .mockResolvedValueOnce({
482
+ message: {
483
+ btc_test_address: 'hidden-state-from-incomplete-status',
484
+ session_id: 'hidden-session-from-incomplete-status',
485
+ },
486
+ });
487
+ const promptPassphrase = jest.fn().mockResolvedValue({ passphrase: 'host hidden wallet' });
488
+ const method = new OpenWalletSession({
489
+ payload: { method: 'openWalletSession', connectId: 'connect-id', mode: 'select-hidden' },
490
+ });
491
+ method.init();
492
+ const device = createDevice({ typedCall, promptPassphrase });
493
+ device.getDeviceState = jest.fn().mockResolvedValue({
494
+ identity: { deviceId: 'device-1' },
495
+ status: {
496
+ unlocked: true,
497
+ passphraseProtection: null,
498
+ unlockedAttachPin: null,
499
+ },
500
+ });
501
+ device.unlockDevice = jest.fn().mockResolvedValue(device.features);
502
+ method.device = device as any;
503
+
504
+ await expect(method.run()).rejects.toMatchObject({
505
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
506
+ });
507
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
508
+ expect(promptPassphrase).not.toHaveBeenCalled();
509
+ });
510
+
396
511
  test('rejects a Pro2 standard-wallet fallback after selecting a hidden wallet', async () => {
397
512
  const typedCall = jest
398
513
  .fn()
@@ -529,7 +644,6 @@ describe('openWalletSession', () => {
529
644
  });
530
645
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
531
646
  session_id: 'known-session',
532
- btc_test_address: 'hidden-state',
533
647
  });
534
648
  expect(promptPassphrase).not.toHaveBeenCalled();
535
649
  });
@@ -741,7 +855,11 @@ describe('openWalletSession', () => {
741
855
  })
742
856
  .mockResolvedValueOnce({
743
857
  identity: { deviceId: 'device-1' },
744
- status: { passphraseProtection: true },
858
+ status: {
859
+ unlocked: true,
860
+ passphraseProtection: true,
861
+ unlockedAttachPin: false,
862
+ },
745
863
  });
746
864
  device.unlockDevice = jest.fn().mockImplementation(() => {
747
865
  device.features.unlocked = true;
@@ -780,14 +898,24 @@ describe('openWalletSession', () => {
780
898
  method.init();
781
899
  const device = createDevice({ typedCall });
782
900
  device.features.unlockedAttachPin = true;
783
- device.getDeviceState = jest.fn().mockResolvedValue({
784
- identity: { deviceId: 'device-1' },
785
- status: {
786
- unlocked: true,
787
- unlockedAttachPin: true,
788
- passphraseProtection: true,
789
- },
790
- });
901
+ device.getDeviceState = jest
902
+ .fn()
903
+ .mockResolvedValueOnce({
904
+ identity: { deviceId: 'device-1' },
905
+ status: {
906
+ unlocked: true,
907
+ unlockedAttachPin: true,
908
+ passphraseProtection: true,
909
+ },
910
+ })
911
+ .mockResolvedValue({
912
+ identity: { deviceId: 'device-1' },
913
+ status: {
914
+ unlocked: true,
915
+ unlockedAttachPin: false,
916
+ passphraseProtection: true,
917
+ },
918
+ });
791
919
  device.unlockDevice = jest.fn().mockImplementation(() => {
792
920
  device.features.unlockedAttachPin = false;
793
921
  return Promise.resolve(device.features);
@@ -1253,7 +1381,6 @@ describe('openWalletSession', () => {
1253
1381
  });
1254
1382
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1255
1383
  session_id: 'known-session',
1256
- btc_test_address: 'hidden-state',
1257
1384
  });
1258
1385
  expect(promptPassphrase).not.toHaveBeenCalled();
1259
1386
  });
@@ -1289,7 +1416,11 @@ describe('openWalletSession', () => {
1289
1416
  })
1290
1417
  .mockResolvedValueOnce({
1291
1418
  identity: { deviceId: 'device-1' },
1292
- status: { passphraseProtection: true },
1419
+ status: {
1420
+ unlocked: true,
1421
+ passphraseProtection: true,
1422
+ unlockedAttachPin: false,
1423
+ },
1293
1424
  });
1294
1425
  device.unlockDevice = jest.fn().mockImplementation(() => {
1295
1426
  device.features.unlocked = true;
@@ -1308,7 +1439,6 @@ describe('openWalletSession', () => {
1308
1439
  expect(device.getDeviceState).toHaveBeenCalledTimes(2);
1309
1440
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1310
1441
  session_id: 'known-session',
1311
- btc_test_address: 'hidden-state',
1312
1442
  });
1313
1443
  const sessionGetCall = typedCall.mock.calls.findIndex(
1314
1444
  ([requestName]) => requestName === 'DeviceSessionGet'
@@ -1346,7 +1476,11 @@ describe('openWalletSession', () => {
1346
1476
  })
1347
1477
  .mockResolvedValueOnce({
1348
1478
  identity: { deviceId: 'other-device' },
1349
- status: { passphraseProtection: true },
1479
+ status: {
1480
+ unlocked: true,
1481
+ passphraseProtection: true,
1482
+ unlockedAttachPin: false,
1483
+ },
1350
1484
  });
1351
1485
  device.unlockDevice = jest.fn().mockImplementation(() => {
1352
1486
  device.features.unlocked = true;
@@ -1401,7 +1535,6 @@ describe('openWalletSession', () => {
1401
1535
  });
1402
1536
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1403
1537
  session_id: 'expired-session',
1404
- btc_test_address: 'hidden-state',
1405
1538
  });
1406
1539
  expect(promptPassphrase).toHaveBeenCalledTimes(1);
1407
1540
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
@@ -1437,26 +1570,12 @@ describe('openWalletSession', () => {
1437
1570
  passphraseState: 'hidden-state',
1438
1571
  resumed: false,
1439
1572
  });
1440
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1441
- btc_test_address: 'hidden-state',
1442
- });
1573
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1443
1574
  expect(promptPassphrase).not.toHaveBeenCalled();
1444
1575
  expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('new-session');
1445
1576
  });
1446
1577
 
1447
- test.each([
1448
- {
1449
- deriveCardano: false,
1450
- seedDomains: [DeviceSessionSeedDomain.SeedDomain_Standard],
1451
- },
1452
- {
1453
- deriveCardano: true,
1454
- seedDomains: [
1455
- DeviceSessionSeedDomain.SeedDomain_Standard,
1456
- DeviceSessionSeedDomain.SeedDomain_Cardano,
1457
- ],
1458
- },
1459
- ])('requests the selected seed domains: $seedDomains', async ({ deriveCardano, seedDomains }) => {
1578
+ test.each([false, true])('keeps deriveCardano API compatibility: %s', async deriveCardano => {
1460
1579
  const typedCall = jest
1461
1580
  .fn()
1462
1581
  .mockResolvedValueOnce({ message: { version: 2 } })
@@ -1485,8 +1604,6 @@ describe('openWalletSession', () => {
1485
1604
  walletType: 'hidden',
1486
1605
  passphraseState: 'hidden-state',
1487
1606
  });
1488
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1489
- seed_domains: seedDomains,
1490
- });
1607
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1491
1608
  });
1492
1609
  });