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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +78 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/deviceUploadNft.test.ts +187 -0
  4. package/__tests__/get-device-state.test.ts +104 -12
  5. package/__tests__/homescreen.test.ts +41 -1
  6. package/__tests__/logBlockEvent.test.ts +1 -1
  7. package/__tests__/open-wallet-session.test.ts +259 -18
  8. package/__tests__/pro2Nft.test.ts +108 -0
  9. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  10. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  11. package/__tests__/protocol-v2.test.ts +377 -402
  12. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  13. package/dist/api/BaseMethod.d.ts +3 -1
  14. package/dist/api/BaseMethod.d.ts.map +1 -1
  15. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  16. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  17. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  18. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  19. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  20. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  21. package/dist/api/index.d.ts +1 -0
  22. package/dist/api/index.d.ts.map +1 -1
  23. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  24. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  26. package/dist/core/deviceEventRegistration.d.ts +2 -0
  27. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/device/Device.d.ts +22 -4
  30. package/dist/device/Device.d.ts.map +1 -1
  31. package/dist/events/device.d.ts +4 -0
  32. package/dist/events/device.d.ts.map +1 -1
  33. package/dist/events/logBlockEvent.d.ts.map +1 -1
  34. package/dist/events/ui-request.d.ts +27 -2
  35. package/dist/events/ui-request.d.ts.map +1 -1
  36. package/dist/index.d.ts +88 -7
  37. package/dist/index.js +937 -604
  38. package/dist/inject.d.ts.map +1 -1
  39. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  40. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  41. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  42. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  43. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/api/index.d.ts +2 -1
  49. package/dist/types/api/index.d.ts.map +1 -1
  50. package/dist/types/api/protocolV2.d.ts +3 -0
  51. package/dist/types/api/protocolV2.d.ts.map +1 -1
  52. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  53. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  54. package/dist/utils/homescreen.d.ts +4 -0
  55. package/dist/utils/homescreen.d.ts.map +1 -1
  56. package/dist/utils/index.d.ts +1 -1
  57. package/dist/utils/index.d.ts.map +1 -1
  58. package/dist/utils/patch.d.ts +1 -1
  59. package/dist/utils/patch.d.ts.map +1 -1
  60. package/dist/utils/pro2Nft.d.ts +29 -0
  61. package/dist/utils/pro2Nft.d.ts.map +1 -0
  62. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  63. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  64. package/package.json +4 -4
  65. package/src/api/BaseMethod.ts +8 -10
  66. package/src/api/FirmwareUpdateV4.ts +13 -6
  67. package/src/api/GetPassphraseState.ts +7 -1
  68. package/src/api/OpenWalletSession.ts +54 -17
  69. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  70. package/src/api/device/DeviceUnlock.ts +1 -1
  71. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  72. package/src/api/index.ts +1 -0
  73. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  74. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  75. package/src/core/deviceEventRegistration.ts +4 -0
  76. package/src/core/index.ts +111 -74
  77. package/src/data/messages/messages-protocol-v2.json +10 -0
  78. package/src/device/Device.ts +221 -24
  79. package/src/events/device.ts +4 -0
  80. package/src/events/logBlockEvent.ts +1 -0
  81. package/src/events/ui-request.ts +32 -2
  82. package/src/inject.ts +1 -0
  83. package/src/protocols/protocol-v2/features.ts +9 -2
  84. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  85. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  86. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  87. package/src/protocols/protocol-v2/walletSession.ts +93 -18
  88. package/src/types/api/index.ts +2 -0
  89. package/src/types/api/protocolV2.ts +13 -0
  90. package/src/utils/deviceFeaturesUtils.ts +4 -0
  91. package/src/utils/homescreen.ts +32 -6
  92. package/src/utils/index.ts +6 -1
  93. package/src/utils/pro2Nft.ts +91 -0
  94. package/src/utils/pro2Wallpaper.ts +35 -8
  95. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  96. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  97. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  98. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  99. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  100. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
@@ -18,6 +18,83 @@ class TestAllNetworkMethod extends AllNetworkGetAddressBase {
18
18
  }
19
19
 
20
20
  describe('AllNetworkGetAddressBase tracing', () => {
21
+ test('resumes a Protocol V2 hidden wallet before running a nested chain method', async () => {
22
+ const calls: string[] = [];
23
+ const checkPassphraseStateSafety = jest.fn().mockImplementation(() => {
24
+ calls.push('resume-hidden-session');
25
+ return Promise.resolve(true);
26
+ });
27
+ const innerMethod = {
28
+ checkSafetyLevelOnTestNet: jest.fn().mockResolvedValue(false),
29
+ connectId: 'connect-id',
30
+ deviceId: 'device-id',
31
+ getVersionRange: jest.fn().mockReturnValue({}),
32
+ assertProtocolSupported: jest.fn(),
33
+ init: jest.fn(),
34
+ name: 'evmGetAddress',
35
+ responseID: 43,
36
+ unlockPolicy: 'unlock-before-run',
37
+ run: jest.fn().mockImplementation(() => {
38
+ calls.push('run-chain-method');
39
+ return Promise.resolve([{ address: '0xhidden' }]);
40
+ }),
41
+ setDevice: jest.fn(),
42
+ strictCheckDeviceSupport: false,
43
+ };
44
+ (findMethod as jest.Mock).mockReturnValue(innerMethod);
45
+ const method = new TestAllNetworkMethod({
46
+ id: 1,
47
+ payload: {
48
+ method: 'allNetworkGetAddress',
49
+ connectId: 'connect-id',
50
+ deviceId: 'device-id',
51
+ passphraseState: 'hidden-state',
52
+ bundle: [],
53
+ },
54
+ });
55
+ method.protocolV2UnlockContext = { preflightCompleted: true };
56
+ const typedCall = jest.fn();
57
+ method.device = {
58
+ checkPassphraseStateSafety,
59
+ commands: {
60
+ typedCall,
61
+ },
62
+ getCurrentFirmwareType: jest.fn(),
63
+ getProtocol: jest.fn().mockReturnValue('V2'),
64
+ getCurrentFirmwareVersionString: jest.fn().mockReturnValue('1.0.0'),
65
+ getCurrentMethodVersionRange: jest
66
+ .fn()
67
+ .mockImplementation((getRange: (type: string) => unknown) => getRange('pro2')),
68
+ instanceId: 'device-instance',
69
+ isProtocolV2: jest.fn().mockReturnValue(true),
70
+ isBootloader: jest.fn().mockReturnValue(false),
71
+ isRomloader: jest.fn().mockReturnValue(false),
72
+ off: jest.fn(),
73
+ on: jest.fn(),
74
+ state: { status: { unlocked: true } },
75
+ updateProtocolV2Status: jest.fn(),
76
+ } as any;
77
+
78
+ await method.callMethod(
79
+ 'evmGetAddress',
80
+ {
81
+ bundle: [
82
+ {
83
+ _originRequestParams: {
84
+ network: 'evm',
85
+ path: "m/44'/60'/0'/0/0",
86
+ },
87
+ },
88
+ ],
89
+ },
90
+ 0
91
+ );
92
+
93
+ expect(checkPassphraseStateSafety).toHaveBeenCalledWith('hidden-state', false, undefined);
94
+ expect(calls).toEqual(['resume-hidden-session', 'run-chain-method']);
95
+ expect(typedCall).not.toHaveBeenCalled();
96
+ });
97
+
21
98
  test('releases the nested request context when an unhandled error escapes', async () => {
22
99
  const deviceInstanceId = 'device-instance';
23
100
  const innerMethod = {
@@ -51,6 +128,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
51
128
  getCurrentMethodVersionRange: jest
52
129
  .fn()
53
130
  .mockImplementation((getRange: (type: string) => unknown) => getRange('classic')),
131
+ isProtocolV2: jest.fn().mockReturnValue(false),
54
132
  off: jest.fn(),
55
133
  on: jest.fn(),
56
134
  } as any;
@@ -3,6 +3,8 @@ import { registerHardwareUiEventListeners } from '../src/core/deviceEventRegistr
3
3
 
4
4
  const handlers = {
5
5
  pin: jest.fn(),
6
+ pinOnDevice: jest.fn(),
7
+ pinOnDeviceComplete: jest.fn(),
6
8
  button: jest.fn(),
7
9
  passphrase: jest.fn(),
8
10
  passphraseOnDevice: jest.fn(),
@@ -21,6 +23,8 @@ describe('hardware UI event registration', () => {
21
23
  expect(registerHardwareUiEventListeners(device as any, handlers)).toBe(true);
22
24
  expect(device.on.mock.calls.map(([type]) => type)).toEqual([
23
25
  DEVICE.PIN,
26
+ DEVICE.PIN_ON_DEVICE,
27
+ DEVICE.PIN_ON_DEVICE_COMPLETE,
24
28
  DEVICE.BUTTON,
25
29
  DEVICE.PASSPHRASE,
26
30
  DEVICE.PASSPHRASE_ON_DEVICE,
@@ -34,6 +38,8 @@ describe('hardware UI event registration', () => {
34
38
  expect(registerHardwareUiEventListeners(device as any, handlers)).toBe(true);
35
39
  expect(device.on.mock.calls.map(([type]) => type)).toEqual([
36
40
  DEVICE.PIN,
41
+ DEVICE.PIN_ON_DEVICE,
42
+ DEVICE.PIN_ON_DEVICE_COMPLETE,
37
43
  DEVICE.BUTTON,
38
44
  DEVICE.PASSPHRASE,
39
45
  DEVICE.PASSPHRASE_ON_DEVICE,
@@ -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,4 +1,4 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import { DeviceType } from '@onekeyfe/hd-transport';
3
3
 
4
4
  import { Device } from '../src/device/Device';
@@ -43,6 +43,70 @@ const getProtocolV2LoaderInfo = (mode: 'bootloader' | 'romloader') => ({
43
43
  });
44
44
 
45
45
  describe('getDeviceState', () => {
46
+ test('coalesces concurrent Protocol V2 runtime-context negotiation', async () => {
47
+ let resolveProtocolInfo:
48
+ | ((value: { message: typeof protocolV2ApplicationInfo }) => void)
49
+ | undefined;
50
+ const typedCall = jest.fn(
51
+ () =>
52
+ new Promise<{ message: typeof protocolV2ApplicationInfo }>(resolve => {
53
+ resolveProtocolInfo = resolve;
54
+ })
55
+ );
56
+ const device = createV2Device(typedCall);
57
+
58
+ const first = device.ensureProtocolV2RuntimeContext();
59
+ const second = device.ensureProtocolV2RuntimeContext();
60
+ resolveProtocolInfo?.({ message: protocolV2ApplicationInfo });
61
+
62
+ await expect(Promise.all([first, second])).resolves.toEqual([
63
+ protocolV2ApplicationInfo,
64
+ protocolV2ApplicationInfo,
65
+ ]);
66
+ expect(typedCall).toHaveBeenCalledTimes(1);
67
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
68
+ eventless_wallet_session: true,
69
+ });
70
+ });
71
+
72
+ test('renegotiates Protocol V2 runtime context after transport disconnect', async () => {
73
+ const typedCall = jest.fn().mockResolvedValue({ message: protocolV2ApplicationInfo });
74
+ const device = createV2Device(typedCall);
75
+
76
+ await device.ensureProtocolV2RuntimeContext();
77
+ await device.ensureProtocolV2RuntimeContext();
78
+ device.markTransportDisconnected();
79
+ await device.ensureProtocolV2RuntimeContext();
80
+
81
+ expect(typedCall).toHaveBeenCalledTimes(2);
82
+ });
83
+
84
+ test('rejects every waiter when runtime-context negotiation is invalidated', async () => {
85
+ let resolveProtocolInfo:
86
+ | ((value: { message: typeof protocolV2ApplicationInfo }) => void)
87
+ | undefined;
88
+ const typedCall = jest.fn(
89
+ () =>
90
+ new Promise<{ message: typeof protocolV2ApplicationInfo }>(resolve => {
91
+ resolveProtocolInfo = resolve;
92
+ })
93
+ );
94
+ const device = createV2Device(typedCall);
95
+
96
+ const first = device.ensureProtocolV2RuntimeContext();
97
+ const second = device.ensureProtocolV2RuntimeContext();
98
+ const firstExpectation = expect(first).rejects.toMatchObject({
99
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
100
+ });
101
+ const secondExpectation = expect(second).rejects.toMatchObject({
102
+ errorCode: HardwareErrorCode.DeviceInitializeFailed,
103
+ });
104
+ device.markTransportDisconnected();
105
+ resolveProtocolInfo?.({ message: protocolV2ApplicationInfo });
106
+
107
+ await Promise.all([firstExpectation, secondExpectation]);
108
+ });
109
+
46
110
  test('does not expose the internal wallet session', async () => {
47
111
  const device = createV2Device(jest.fn());
48
112
  device.updateState({ protocol: 'V2' }, 'initialize');
@@ -159,15 +223,20 @@ describe('getDeviceState', () => {
159
223
  throw new Error(`Unexpected request: ${requestType}`);
160
224
  });
161
225
  const device = createV2Device(typedCall);
162
- device.updateState({ protocol: 'V2', status: { mode } }, 'initialize');
226
+ device.updateState(
227
+ {
228
+ protocol: 'V2',
229
+ identity: { deviceType: EDeviceType.Pro2 },
230
+ status: { mode },
231
+ raw: { protocolV2ProtocolInfo: getProtocolV2LoaderInfo(mode) },
232
+ },
233
+ 'initialize'
234
+ );
163
235
 
164
236
  const state = await device.getDeviceState({ refreshSections: ['status'] });
165
237
 
166
238
  expect(state.status.mode).toBe(mode);
167
- expect(typedCall.mock.calls.map(call => call[0])).toEqual([
168
- 'DeviceInfoGet',
169
- 'ProtocolInfoRequest',
170
- ]);
239
+ expect(typedCall).not.toHaveBeenCalled();
171
240
  }
172
241
  );
173
242
 
@@ -187,7 +256,14 @@ describe('getDeviceState', () => {
187
256
  return { message: { init_states: true, unlocked: true, device_id: 'device-1' } };
188
257
  });
189
258
  const device = createV2Device(typedCall);
190
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
259
+ device.updateState(
260
+ {
261
+ protocol: 'V2',
262
+ status: { mode: 'normal' },
263
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
264
+ },
265
+ 'initialize'
266
+ );
191
267
 
192
268
  const state = await device.getDeviceState({ refreshSections: ['status'] });
193
269
 
@@ -205,7 +281,14 @@ describe('getDeviceState', () => {
205
281
  },
206
282
  });
207
283
  const device = createV2Device(typedCall);
208
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
284
+ device.updateState(
285
+ {
286
+ protocol: 'V2',
287
+ status: { mode: 'normal' },
288
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
289
+ },
290
+ 'initialize'
291
+ );
209
292
  const onState = jest.fn();
210
293
  device.on(DEVICE.STATE, onState);
211
294
 
@@ -250,13 +333,18 @@ describe('getDeviceState', () => {
250
333
  throw new Error(`Unexpected request: ${requestType}`);
251
334
  });
252
335
  const device = createV2Device(typedCall);
253
- device.updateState({ protocol: 'V2', status: { mode: 'normal' } }, 'initialize');
336
+ device.updateState(
337
+ {
338
+ protocol: 'V2',
339
+ status: { mode: 'normal' },
340
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
341
+ },
342
+ 'initialize'
343
+ );
254
344
 
255
345
  const state = await device.getDeviceState({ refreshSections: ['status', 'settings'] });
256
346
 
257
347
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
258
- 'DeviceInfoGet',
259
- 'ProtocolInfoRequest',
260
348
  'DeviceStatusGet',
261
349
  'DeviceSettingsGet',
262
350
  ]);
@@ -288,7 +376,11 @@ describe('getDeviceState', () => {
288
376
  });
289
377
  const device = createV2Device(typedCall);
290
378
  device.updateState(
291
- { protocol: 'V2', status: { mode: 'normal', unlocked: true } },
379
+ {
380
+ protocol: 'V2',
381
+ status: { mode: 'normal', unlocked: true },
382
+ raw: { protocolV2ProtocolInfo: protocolV2ApplicationInfo },
383
+ },
292
384
  'initialize'
293
385
  );
294
386
 
@@ -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) };