@onekeyfe/hd-core 1.2.0-alpha.41 → 1.2.0-alpha.42

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 (200) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +78 -0
  2. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +14 -1
  4. package/__tests__/connectSettings.test.ts +5 -47
  5. package/__tests__/deviceUploadNft.test.ts +187 -0
  6. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +31 -59
  7. package/__tests__/get-device-state.test.ts +132 -12
  8. package/__tests__/homescreen.test.ts +41 -1
  9. package/__tests__/logBlockEvent.test.ts +1 -1
  10. package/__tests__/open-wallet-session.test.ts +259 -18
  11. package/__tests__/pro2Nft.test.ts +108 -0
  12. package/__tests__/protocol-v2-resources.test.ts +204 -0
  13. package/__tests__/protocol-v2-ui-lifecycle.test.ts +56 -0
  14. package/__tests__/protocol-v2-unlock-policy.test.ts +86 -35
  15. package/__tests__/protocol-v2.test.ts +526 -955
  16. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  17. package/__tests__/uiProgressThrottle.test.ts +74 -0
  18. package/dist/api/BaseMethod.d.ts +3 -1
  19. package/dist/api/BaseMethod.d.ts.map +1 -1
  20. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  22. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  24. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV4.d.ts +6 -25
  26. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  27. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  28. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  29. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  30. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -8
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  34. package/dist/api/firmware/getBinary.d.ts +0 -7
  35. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  36. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  37. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  38. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  39. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  40. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  41. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  42. package/dist/api/index.d.ts +1 -0
  43. package/dist/api/index.d.ts.map +1 -1
  44. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +30 -0
  45. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  46. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/core/deviceEventRegistration.d.ts +2 -0
  49. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  50. package/dist/core/index.d.ts.map +1 -1
  51. package/dist/data-manager/DataManager.d.ts +3 -2
  52. package/dist/data-manager/DataManager.d.ts.map +1 -1
  53. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  54. package/dist/device/Device.d.ts +22 -4
  55. package/dist/device/Device.d.ts.map +1 -1
  56. package/dist/events/device.d.ts +4 -0
  57. package/dist/events/device.d.ts.map +1 -1
  58. package/dist/events/logBlockEvent.d.ts.map +1 -1
  59. package/dist/events/ui-request.d.ts +27 -2
  60. package/dist/events/ui-request.d.ts.map +1 -1
  61. package/dist/index.d.ts +110 -182
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1919 -3336
  64. package/dist/inject.d.ts.map +1 -1
  65. package/dist/lowLevelInject.d.ts.map +1 -1
  66. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  67. package/dist/protocols/protocol-v2/resources.d.ts +30 -0
  68. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
  69. package/dist/protocols/protocol-v2/uiInteraction.d.ts +3 -3
  70. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  71. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  72. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  74. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  75. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  76. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  77. package/dist/topLevelInject.d.ts.map +1 -1
  78. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -4
  79. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  80. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  81. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  82. package/dist/types/api/export.d.ts +1 -4
  83. package/dist/types/api/export.d.ts.map +1 -1
  84. package/dist/types/api/firmwareUpdate.d.ts +0 -69
  85. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  86. package/dist/types/api/index.d.ts +3 -10
  87. package/dist/types/api/index.d.ts.map +1 -1
  88. package/dist/types/api/protocolV2.d.ts +3 -0
  89. package/dist/types/api/protocolV2.d.ts.map +1 -1
  90. package/dist/types/settings.d.ts +12 -13
  91. package/dist/types/settings.d.ts.map +1 -1
  92. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  93. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  94. package/dist/utils/homescreen.d.ts +4 -0
  95. package/dist/utils/homescreen.d.ts.map +1 -1
  96. package/dist/utils/index.d.ts +1 -1
  97. package/dist/utils/index.d.ts.map +1 -1
  98. package/dist/utils/patch.d.ts +1 -1
  99. package/dist/utils/patch.d.ts.map +1 -1
  100. package/dist/utils/pro2Nft.d.ts +29 -0
  101. package/dist/utils/pro2Nft.d.ts.map +1 -0
  102. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  103. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  104. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  105. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  106. package/package.json +4 -4
  107. package/src/api/BaseMethod.ts +8 -10
  108. package/src/api/CheckAllFirmwareRelease.ts +40 -53
  109. package/src/api/FirmwareUpdateV2.ts +120 -298
  110. package/src/api/FirmwareUpdateV3.ts +57 -263
  111. package/src/api/FirmwareUpdateV4.ts +307 -912
  112. package/src/api/GetPassphraseState.ts +7 -1
  113. package/src/api/OpenWalletSession.ts +54 -17
  114. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  115. package/src/api/device/DeviceUnlock.ts +1 -1
  116. package/src/api/device/DeviceUpdateBootloader.ts +6 -122
  117. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -57
  118. package/src/api/firmware/updateBootloader.ts +4 -19
  119. package/src/api/firmware/uploadFirmware.ts +22 -140
  120. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  121. package/src/api/index.ts +1 -0
  122. package/src/api/protocol-v2/DeviceUploadNft.ts +166 -0
  123. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  124. package/src/core/deviceEventRegistration.ts +4 -0
  125. package/src/core/index.ts +118 -75
  126. package/src/data/messages/messages-protocol-v2.json +53 -0
  127. package/src/data-manager/DataManager.ts +63 -38
  128. package/src/data-manager/connectSettings.ts +0 -11
  129. package/src/device/Device.ts +193 -21
  130. package/src/events/device.ts +4 -0
  131. package/src/events/logBlockEvent.ts +1 -0
  132. package/src/events/ui-request.ts +32 -2
  133. package/src/index.ts +0 -5
  134. package/src/inject.ts +3 -22
  135. package/src/lowLevelInject.ts +1 -5
  136. package/src/protocols/protocol-v2/features.ts +9 -2
  137. package/src/protocols/protocol-v2/resources.ts +233 -0
  138. package/src/protocols/protocol-v2/uiInteraction.ts +31 -5
  139. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  140. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +117 -0
  141. package/src/protocols/protocol-v2/walletSession.ts +93 -18
  142. package/src/topLevelInject.ts +1 -5
  143. package/src/types/api/checkAllFirmwareRelease.ts +1 -4
  144. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  145. package/src/types/api/export.ts +0 -18
  146. package/src/types/api/firmwareUpdate.ts +0 -76
  147. package/src/types/api/index.ts +2 -14
  148. package/src/types/api/protocolV2.ts +13 -0
  149. package/src/types/settings.ts +26 -13
  150. package/src/utils/deviceFeaturesUtils.ts +4 -0
  151. package/src/utils/homescreen.ts +32 -6
  152. package/src/utils/index.ts +6 -1
  153. package/src/utils/pro2Nft.ts +91 -0
  154. package/src/utils/pro2Wallpaper.ts +35 -8
  155. package/src/utils/uiProgressThrottle.ts +63 -0
  156. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -175
  157. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  158. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  159. package/__tests__/firmware-update/firmware-preparation-error.test.ts +0 -27
  160. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  161. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -593
  162. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -213
  163. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +0 -58
  164. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  165. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  166. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  167. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  168. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  169. package/dist/api/firmware/FirmwarePreparationError.d.ts +0 -3
  170. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +0 -1
  171. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  172. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  173. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -25
  174. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  175. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -30
  176. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  177. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  178. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  179. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  180. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  181. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  182. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  183. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  184. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  185. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -28
  186. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  187. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  188. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  189. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  190. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  191. package/src/api/firmware/FirmwarePreparationError.ts +0 -12
  192. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  193. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -745
  194. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -392
  195. package/src/api/firmware/progressThrottle.ts +0 -44
  196. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  197. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
  198. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  199. package/src/types/api/firmwareUpdatePlan.ts +0 -38
  200. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -0,0 +1,108 @@
1
+ import { blake2s } from '@noble/hashes/blake2s';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+
4
+ import {
5
+ PRO2_NFT_IMAGE_HEIGHT,
6
+ PRO2_NFT_IMAGE_WIDTH,
7
+ PRO2_NFT_THUMBNAIL_HEIGHT,
8
+ PRO2_NFT_THUMBNAIL_WIDTH,
9
+ buildPro2NftBundle,
10
+ } from '../src/utils/pro2Nft';
11
+
12
+ const rgba = (width: number, height: number, alpha = 0xff) => {
13
+ const data = new Uint8Array(width * height * 4);
14
+ for (let index = 3; index < data.length; index += 4) data[index] = alpha;
15
+ return data;
16
+ };
17
+
18
+ describe('buildPro2NftBundle', () => {
19
+ test('builds the exact Pro2 RGB565 triplet and basename', () => {
20
+ const bundle = buildPro2NftBundle({
21
+ image: {
22
+ width: PRO2_NFT_IMAGE_WIDTH,
23
+ height: PRO2_NFT_IMAGE_HEIGHT,
24
+ rgba: rgba(PRO2_NFT_IMAGE_WIDTH, PRO2_NFT_IMAGE_HEIGHT),
25
+ },
26
+ thumbnail: {
27
+ width: PRO2_NFT_THUMBNAIL_WIDTH,
28
+ height: PRO2_NFT_THUMBNAIL_HEIGHT,
29
+ rgba: rgba(PRO2_NFT_THUMBNAIL_WIDTH, PRO2_NFT_THUMBNAIL_HEIGHT),
30
+ },
31
+ title: 'CryptoPunk #3100',
32
+ subtitle: 'CryptoPunks',
33
+ timestampMs: 1_760_000_000_000,
34
+ });
35
+
36
+ expect(bundle.image.byteLength).toBe(583_212);
37
+ expect(bundle.thumbnail.byteLength).toBe(138_876);
38
+ expect(Array.from(bundle.image.slice(0, 12))).toEqual([
39
+ 0x19, 0x12, 0, 0, 28, 2, 28, 2, 56, 4, 0, 0,
40
+ ]);
41
+ expect(Array.from(bundle.thumbnail.slice(0, 12))).toEqual([
42
+ 0x19, 0x12, 0, 0, 7, 1, 7, 1, 16, 2, 0, 0,
43
+ ]);
44
+ expect(new TextDecoder().decode(bundle.metadata)).toBe(
45
+ '{"title":"CryptoPunk #3100","subtitle":"CryptoPunks"}'
46
+ );
47
+ expect(bytesToHex(blake2s(bundle.image))).toBe(
48
+ 'af4b5f8fe216cfd0ef4a9162a42b5c66a73466f2491066f66f756c4e28cc869e'
49
+ );
50
+ expect(bytesToHex(blake2s(bundle.thumbnail))).toBe(
51
+ '44591afae1163cc14651d77644501c6a110f96506889b825cab0351ff4de857d'
52
+ );
53
+ expect(bytesToHex(blake2s(bundle.metadata))).toBe(
54
+ 'e30d4dc2cddd441c9f1438c783e05a623ccf76f2f6219d4281769dda1b3635b5'
55
+ );
56
+ expect(bundle.basename).toBe('nft-af4b5f8f-1760000000000');
57
+ });
58
+
59
+ test('composites transparent pixels onto black without an alpha plane', () => {
60
+ const image = rgba(PRO2_NFT_IMAGE_WIDTH, PRO2_NFT_IMAGE_HEIGHT);
61
+ image.set([255, 255, 255, 0], 0);
62
+ const bundle = buildPro2NftBundle({
63
+ image: { width: PRO2_NFT_IMAGE_WIDTH, height: PRO2_NFT_IMAGE_HEIGHT, rgba: image },
64
+ thumbnail: {
65
+ width: PRO2_NFT_THUMBNAIL_WIDTH,
66
+ height: PRO2_NFT_THUMBNAIL_HEIGHT,
67
+ rgba: rgba(PRO2_NFT_THUMBNAIL_WIDTH, PRO2_NFT_THUMBNAIL_HEIGHT, 0),
68
+ },
69
+ title: 'NFT',
70
+ subtitle: '',
71
+ timestampMs: 1,
72
+ });
73
+
74
+ expect(bundle.image[1]).toBe(0x12);
75
+ expect(Array.from(bundle.image.slice(12, 14))).toEqual([0, 0]);
76
+ expect(bundle.thumbnail.byteLength).toBe(138_876);
77
+ });
78
+
79
+ test('rejects invalid dimensions and metadata byte lengths', () => {
80
+ const validThumbnail = {
81
+ width: PRO2_NFT_THUMBNAIL_WIDTH,
82
+ height: PRO2_NFT_THUMBNAIL_HEIGHT,
83
+ rgba: rgba(PRO2_NFT_THUMBNAIL_WIDTH, PRO2_NFT_THUMBNAIL_HEIGHT),
84
+ };
85
+ expect(() =>
86
+ buildPro2NftBundle({
87
+ image: { width: 1, height: 1, rgba: new Uint8Array(4) },
88
+ thumbnail: validThumbnail,
89
+ title: 'NFT',
90
+ subtitle: '',
91
+ timestampMs: 1,
92
+ })
93
+ ).toThrow('540x540');
94
+ expect(() =>
95
+ buildPro2NftBundle({
96
+ image: {
97
+ width: PRO2_NFT_IMAGE_WIDTH,
98
+ height: PRO2_NFT_IMAGE_HEIGHT,
99
+ rgba: rgba(PRO2_NFT_IMAGE_WIDTH, PRO2_NFT_IMAGE_HEIGHT),
100
+ },
101
+ thumbnail: validThumbnail,
102
+ title: 'a'.repeat(64),
103
+ subtitle: '',
104
+ timestampMs: 1,
105
+ })
106
+ ).toThrow('1 to 63');
107
+ });
108
+ });
@@ -0,0 +1,204 @@
1
+ import axios from 'axios';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+
4
+ import { DataManager } from '../src/data-manager';
5
+ import {
6
+ PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
7
+ PROTOCOL_V2_RESOURCE_TYPES,
8
+ buildProtocolV2ResourceUpdatePlan,
9
+ isProtocolV2ResourceFileValid,
10
+ parseProtocolV2ResourceInventory,
11
+ parseProtocolV2Resources,
12
+ requestProtocolV2ResourceInventory,
13
+ } from '../src/protocols/protocol-v2/resources';
14
+
15
+ import type { ConnectSettings, IProtocolV2Resource, RemoteConfigResponse } from '../src/types';
16
+
17
+ jest.mock('axios');
18
+ jest.mock('../src/data/config', () => ({
19
+ getSDKVersion: jest.fn(() => '1.0.0-test'),
20
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0-test/',
21
+ }));
22
+
23
+ const bytesToHex = (bytes: Uint8Array) =>
24
+ Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
25
+
26
+ const resources: IProtocolV2Resource[] = PROTOCOL_V2_RESOURCE_TYPES.map((type, index) => ({
27
+ type,
28
+ url: `https://example.com/${type}.okpkg`,
29
+ size: index + 100,
30
+ fileHash: index.toString(16).padStart(64, '0'),
31
+ headerHash: index.toString(16).padStart(128, '0'),
32
+ }));
33
+
34
+ const createSettings = (configFetcher: ConnectSettings['configFetcher']): ConnectSettings =>
35
+ ({
36
+ env: 'node',
37
+ fetchConfig: true,
38
+ preRelease: true,
39
+ configFetcher,
40
+ } as ConnectSettings);
41
+
42
+ const createRemoteConfig = (): RemoteConfigResponse =>
43
+ ({
44
+ classic: { firmware: [], ble: [] },
45
+ classic1s: { firmware: [], ble: [] },
46
+ classicpure: { firmware: [], ble: [] },
47
+ mini: { firmware: [], ble: [] },
48
+ touch: { firmware: [], ble: [] },
49
+ pro: { firmware: [], ble: [] },
50
+ pro2: { firmware: [], ble: [], resources: { stable: resources } },
51
+ bridge: {},
52
+ } as unknown as RemoteConfigResponse);
53
+
54
+ describe('Pro2 resource configuration', () => {
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ DataManager.lastCheckTimestamp = 0;
58
+ });
59
+
60
+ test('accepts exactly six resources and normalizes their deterministic order', () => {
61
+ const parsed = parseProtocolV2Resources({ stable: [...resources].reverse() });
62
+
63
+ expect(parsed?.stable.map(item => item.type)).toEqual(PROTOCOL_V2_RESOURCE_TYPES);
64
+ expect(PROTOCOL_V2_RESOURCE_DEVICE_PATHS.translations).toBe(
65
+ 'vol0:/bundles/translations/translations.okpkg'
66
+ );
67
+ });
68
+
69
+ test('rejects incomplete, duplicate, or malformed stable sets', () => {
70
+ expect(() => parseProtocolV2Resources({ stable: resources.slice(1) })).toThrow(
71
+ 'six unique resource types'
72
+ );
73
+ expect(() =>
74
+ parseProtocolV2Resources({ stable: [...resources.slice(0, 5), resources[0]] })
75
+ ).toThrow('six unique resource types');
76
+ expect(() =>
77
+ parseProtocolV2Resources({
78
+ stable: resources.map((resource, index) =>
79
+ index === 0 ? { ...resource, headerHash: 'bad' } : resource
80
+ ),
81
+ })
82
+ ).toThrow('headerHash');
83
+ });
84
+
85
+ test('downloads nothing when all resource identities match', () => {
86
+ const inventory = resources.map(({ type, size, headerHash }) => ({ type, size, headerHash }));
87
+
88
+ expect(
89
+ buildProtocolV2ResourceUpdatePlan({ resources, inventory, mode: 'application' })
90
+ ).toEqual({ status: 'valid', resources: [] });
91
+ });
92
+
93
+ test('normalizes the success-only ResourceInventory RPC response', async () => {
94
+ const items = resources.slice(0, 2).map((resource, index) => ({
95
+ type: index === 0 ? 'IMAGES' : 1,
96
+ size: resource.size,
97
+ header_hash: resource.headerHash,
98
+ }));
99
+ const typedCall = jest.fn().mockResolvedValue({ message: { items } });
100
+
101
+ await expect(
102
+ requestProtocolV2ResourceInventory({ commands: { typedCall } as any })
103
+ ).resolves.toEqual([
104
+ {
105
+ type: 'images',
106
+ size: resources[0].size,
107
+ headerHash: resources[0].headerHash,
108
+ },
109
+ {
110
+ type: 'animation',
111
+ size: resources[1].size,
112
+ headerHash: resources[1].headerHash,
113
+ },
114
+ ]);
115
+ expect(typedCall).toHaveBeenCalledWith(
116
+ 'ResourceInventoryGet',
117
+ 'ResourceInventory',
118
+ {},
119
+ { timeoutMs: 5000 }
120
+ );
121
+ });
122
+
123
+ test('rejects malformed or duplicate inventory identities', () => {
124
+ expect(() =>
125
+ parseProtocolV2ResourceInventory({
126
+ items: [
127
+ { type: 'IMAGES', size: 1, header_hash: 'a'.repeat(128) },
128
+ { type: 'IMAGES', size: 1, header_hash: 'b'.repeat(128) },
129
+ ],
130
+ })
131
+ ).toThrow('duplicate resource type');
132
+ expect(() =>
133
+ parseProtocolV2ResourceInventory({
134
+ items: [{ type: 'IMAGES', size: 1, header_hash: 'bad' }],
135
+ })
136
+ ).toThrow('inventory headerHash');
137
+ });
138
+
139
+ test('selects only the changed or missing resource in application mode', () => {
140
+ const inventory = resources
141
+ .filter(resource => resource.type !== 'noto')
142
+ .map(({ type, size, headerHash }) => ({
143
+ type,
144
+ size: type === 'images' ? size + 1 : size,
145
+ headerHash,
146
+ }));
147
+
148
+ const result = buildProtocolV2ResourceUpdatePlan({
149
+ resources,
150
+ inventory,
151
+ mode: 'application',
152
+ });
153
+
154
+ expect(result.status).toBe('outdated');
155
+ expect(result.resources.map(resource => resource.type)).toEqual(['images', 'noto']);
156
+ });
157
+
158
+ test('reports unknown without an application inventory and selects all in recovery mode', () => {
159
+ expect(buildProtocolV2ResourceUpdatePlan({ resources, mode: 'application' })).toEqual({
160
+ status: 'unknown',
161
+ resources: [],
162
+ });
163
+ expect(
164
+ buildProtocolV2ResourceUpdatePlan({ resources, mode: 'bootloader-recovery' }).resources
165
+ ).toHaveLength(6);
166
+ });
167
+
168
+ test('verifies both full file size and SHA-256 before transfer', () => {
169
+ const bytes = new Uint8Array([1, 2, 3]);
170
+ const binary = bytes.buffer;
171
+ const identity = { size: bytes.byteLength, fileHash: bytesToHex(sha256(bytes)) };
172
+
173
+ expect(isProtocolV2ResourceFileValid(binary, identity)).toBe(true);
174
+ expect(isProtocolV2ResourceFileValid(binary, { ...identity, size: 4 })).toBe(false);
175
+ expect(isProtocolV2ResourceFileValid(binary, { ...identity, fileHash: '0'.repeat(64) })).toBe(
176
+ false
177
+ );
178
+ });
179
+
180
+ test('applies a validated pre-release config and exposes the stable resource set', async () => {
181
+ const configFetcher = jest.fn().mockResolvedValue(createRemoteConfig());
182
+
183
+ await expect(DataManager.load(createSettings(configFetcher))).resolves.toBe(true);
184
+
185
+ expect(configFetcher).toHaveBeenCalledWith(
186
+ expect.stringMatching(/^https:\/\/data\.onekey\.so\/pre-config\.json\?noCache=/)
187
+ );
188
+ expect(DataManager.getProtocolV2Resources()).toEqual(resources);
189
+ });
190
+
191
+ test('does not advance the cache timestamp when refresh fails', async () => {
192
+ jest.spyOn(axios, 'get').mockRejectedValue(new Error('offline'));
193
+ const settings = createSettings(jest.fn().mockResolvedValue(null));
194
+ DataManager.settings = settings;
195
+
196
+ await DataManager.checkAndReloadData();
197
+
198
+ expect(DataManager.lastCheckTimestamp).toBe(0);
199
+ await expect(DataManager.forceReloadData()).rejects.toThrow(
200
+ 'Unable to refresh the latest remote config'
201
+ );
202
+ expect(DataManager.lastCheckTimestamp).toBe(0);
203
+ });
204
+ });
@@ -0,0 +1,56 @@
1
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
2
+
3
+ import { DEVICE } from '../src/events';
4
+ import { Device } from '../src/device/Device';
5
+
6
+ jest.mock('../src/data/config', () => ({
7
+ getSDKVersion: jest.fn(() => '1.0.0-test'),
8
+ DEFAULT_DOMAIN: 'https://example.com/',
9
+ }));
10
+
11
+ describe('Protocol V2 UI interaction lifecycle', () => {
12
+ test('emits a matching PIN phase completion after DeviceSessionAskPin succeeds', async () => {
13
+ const device = new Device({ id: 'connect-1' } as any);
14
+ device.isProtocolV2 = jest.fn(() => true);
15
+ device.updateProtocolV2Status = jest.fn(() => undefined as any);
16
+ device.commands = {
17
+ typedCall: jest.fn().mockResolvedValue({
18
+ message: { unlocked: true },
19
+ }),
20
+ } as any;
21
+ device.beginProtocolV2UiInteraction();
22
+
23
+ const starts: unknown[] = [];
24
+ const completions: unknown[] = [];
25
+ device.on(DEVICE.PIN_ON_DEVICE, (...args) => starts.push(args));
26
+ device.on(DEVICE.PIN_ON_DEVICE_COMPLETE, (...args) => completions.push(args));
27
+
28
+ await device.unlockDevice(DeviceSessionPinType.Main, {
29
+ source: 'unlock-coordinator',
30
+ reason: 'device-locked',
31
+ deviceOnly: true,
32
+ });
33
+
34
+ const startMetadata = (starts[0] as unknown[])[2] as Record<string, unknown>;
35
+ const completionMetadata = (completions[0] as unknown[])[1] as Record<string, unknown>;
36
+ expect(startMetadata.interaction).toMatchObject({
37
+ phase: 'pin',
38
+ transition: 'start',
39
+ protocol: 'V2',
40
+ });
41
+ expect(completionMetadata).toMatchObject({
42
+ interactionId: (startMetadata.interaction as { interactionId: string }).interactionId,
43
+ phaseId: (startMetadata.interaction as { phaseId: string }).phaseId,
44
+ phase: 'pin',
45
+ transition: 'complete',
46
+ outcome: 'succeeded',
47
+ protocol: 'V2',
48
+ });
49
+ expect(completionMetadata.sequence).toBeGreaterThan(
50
+ (startMetadata.interaction as { sequence: number }).sequence
51
+ );
52
+ expect(completionMetadata.phaseId).toBe(
53
+ (startMetadata.interaction as { phaseId: string }).phaseId
54
+ );
55
+ });
56
+ });
@@ -1,45 +1,96 @@
1
- import * as publicMethods from '../src/api';
2
- import {
3
- PROTOCOL_V2_RETRY_ON_LOCKED_METHODS,
4
- getProtocolV2UnlockPolicy,
5
- } from '../src/protocols/protocol-v2/unlockPolicy';
1
+ import ConfluxSignMessageCIP23 from '../src/api/conflux/ConfluxSignMessageCIP23';
2
+ import DeviceLock from '../src/api/device/DeviceLock';
3
+ import { runMethodWithUnlockPolicy } from '../src/protocols/protocol-v2/unlockPolicyRunner';
6
4
 
7
5
  jest.mock('../src/data/config', () => ({
8
6
  DEFAULT_DOMAIN: 'https://example.com/',
9
7
  getSDKVersion: () => '0.0.0-test',
10
8
  }));
11
9
 
12
- describe('Protocol V2 public method unlock policy', () => {
13
- test('classifies the complete public method table with an explicit replay allowlist', () => {
14
- const publicMethodNames = Object.keys(publicMethods);
15
- const retryMethodNames = new Set<string>(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS);
16
-
17
- expect(publicMethodNames.length).toBeGreaterThan(100);
18
- expect(new Set(PROTOCOL_V2_RETRY_ON_LOCKED_METHODS).size).toBe(
19
- PROTOCOL_V2_RETRY_ON_LOCKED_METHODS.length
20
- );
21
-
22
- for (const methodName of PROTOCOL_V2_RETRY_ON_LOCKED_METHODS) {
23
- expect(publicMethods).toHaveProperty(methodName);
24
- }
25
-
26
- for (const methodName of publicMethodNames) {
27
- expect(getProtocolV2UnlockPolicy(methodName)).toBe(
28
- retryMethodNames.has(methodName) ? 'retry-on-locked' : 'none'
29
- );
30
- }
10
+ describe('Protocol V2 unlock semantics', () => {
11
+ test('wallet business methods inherit the wallet-session unlock requirement', () => {
12
+ const method = new ConfluxSignMessageCIP23({
13
+ id: 1,
14
+ payload: { method: 'confluxSignMessageCIP23' },
15
+ });
16
+
17
+ expect(method.useDevicePassphraseState).toBe(true);
18
+ expect(method.unlockPolicy).toBe('none');
19
+ expect(method.getSupportedProtocols()).toContain('V2');
20
+ });
21
+
22
+ test('lock-free Protocol V2 controls explicitly opt out of wallet-session handling', () => {
23
+ const method = new DeviceLock({
24
+ id: 1,
25
+ payload: { method: 'deviceLock' },
26
+ });
27
+
28
+ method.init();
29
+
30
+ expect(method.useDevicePassphraseState).toBe(false);
31
+ expect(method.unlockPolicy).toBe('none');
31
32
  });
32
33
 
33
- test.each([
34
- 'deviceUnlock',
35
- 'deviceLock',
36
- 'deviceWipe',
37
- 'deviceChangePin',
38
- 'deviceSettings',
39
- 'firmwareUpdateV4',
40
- 'deviceUploadWallpaper',
41
- 'uploadPortfolio',
42
- ])('does not make the state-changing method %s replayable by default', methodName => {
43
- expect(getProtocolV2UnlockPolicy(methodName)).toBe('none');
34
+ test('pre-unlocks an unregistered wallet business method', async () => {
35
+ const features = { unlocked: false };
36
+ const method = {
37
+ name: 'newWalletBusinessMethod',
38
+ unlockPolicy: 'none',
39
+ useDevicePassphraseState: true,
40
+ run: jest.fn().mockResolvedValue({ message: 'ok' }),
41
+ };
42
+ const device = {
43
+ features,
44
+ commands: {
45
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
46
+ },
47
+ isProtocolV2: () => true,
48
+ isBootloader: () => false,
49
+ isRomloader: () => false,
50
+ updateProtocolV2Status: jest.fn(() => features),
51
+ unlockDevice: jest.fn().mockImplementation(() => {
52
+ features.unlocked = true;
53
+ return Promise.resolve(features);
54
+ }),
55
+ };
56
+
57
+ await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
58
+ message: 'ok',
59
+ });
60
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
61
+ expect(method.run).toHaveBeenCalledTimes(1);
62
+ });
63
+
64
+ test('runs fresh-status validation before starting unlock', async () => {
65
+ const identityError = new Error('Unexpected device');
66
+ const method = {
67
+ name: 'walletBusinessMethod',
68
+ unlockPolicy: 'none',
69
+ useDevicePassphraseState: true,
70
+ run: jest.fn(),
71
+ };
72
+ const features = { unlocked: false };
73
+ const device = {
74
+ features,
75
+ commands: {
76
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
77
+ },
78
+ isProtocolV2: () => true,
79
+ isBootloader: () => false,
80
+ isRomloader: () => false,
81
+ updateProtocolV2Status: jest.fn(() => features),
82
+ unlockDevice: jest.fn(),
83
+ };
84
+
85
+ await expect(
86
+ runMethodWithUnlockPolicy(method as any, device as any, {
87
+ afterStatusBeforeUnlock: () => {
88
+ throw identityError;
89
+ },
90
+ })
91
+ ).rejects.toBe(identityError);
92
+
93
+ expect(device.unlockDevice).not.toHaveBeenCalled();
94
+ expect(method.run).not.toHaveBeenCalled();
44
95
  });
45
96
  });