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

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 (202) 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 +272 -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 +366 -31
  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 +91 -0
  14. package/__tests__/protocol-v2-unlock-policy.test.ts +196 -35
  15. package/__tests__/protocol-v2.test.ts +568 -957
  16. package/__tests__/protocolV2UiInteraction.test.ts +63 -0
  17. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  18. package/__tests__/uiProgressThrottle.test.ts +74 -0
  19. package/dist/api/BaseMethod.d.ts +3 -1
  20. package/dist/api/BaseMethod.d.ts.map +1 -1
  21. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  23. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  25. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  26. package/dist/api/FirmwareUpdateV4.d.ts +6 -25
  27. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  28. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  29. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  30. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  32. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -8
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/getBinary.d.ts +0 -7
  36. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  37. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  38. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  39. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  40. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  41. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  42. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  43. package/dist/api/index.d.ts +1 -0
  44. package/dist/api/index.d.ts.map +1 -1
  45. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +31 -0
  46. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  47. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  48. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  49. package/dist/core/deviceEventRegistration.d.ts +2 -0
  50. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/data-manager/DataManager.d.ts +3 -2
  53. package/dist/data-manager/DataManager.d.ts.map +1 -1
  54. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  55. package/dist/device/Device.d.ts +24 -4
  56. package/dist/device/Device.d.ts.map +1 -1
  57. package/dist/device/DeviceCommands.d.ts +4 -4
  58. package/dist/events/device.d.ts +4 -0
  59. package/dist/events/device.d.ts.map +1 -1
  60. package/dist/events/logBlockEvent.d.ts.map +1 -1
  61. package/dist/events/ui-request.d.ts +28 -2
  62. package/dist/events/ui-request.d.ts.map +1 -1
  63. package/dist/index.d.ts +115 -184
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2051 -3349
  66. package/dist/inject.d.ts.map +1 -1
  67. package/dist/lowLevelInject.d.ts.map +1 -1
  68. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  69. package/dist/protocols/protocol-v2/resources.d.ts +30 -0
  70. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
  71. package/dist/protocols/protocol-v2/uiInteraction.d.ts +7 -4
  72. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  73. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  74. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  75. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  76. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  77. package/dist/protocols/protocol-v2/walletSession.d.ts +4 -1
  78. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  79. package/dist/topLevelInject.d.ts.map +1 -1
  80. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -4
  81. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  82. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  83. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  84. package/dist/types/api/export.d.ts +1 -4
  85. package/dist/types/api/export.d.ts.map +1 -1
  86. package/dist/types/api/firmwareUpdate.d.ts +0 -69
  87. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  88. package/dist/types/api/index.d.ts +3 -10
  89. package/dist/types/api/index.d.ts.map +1 -1
  90. package/dist/types/api/protocolV2.d.ts +3 -0
  91. package/dist/types/api/protocolV2.d.ts.map +1 -1
  92. package/dist/types/settings.d.ts +12 -13
  93. package/dist/types/settings.d.ts.map +1 -1
  94. package/dist/utils/deviceFeaturesUtils.d.ts +2 -0
  95. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  96. package/dist/utils/homescreen.d.ts +4 -0
  97. package/dist/utils/homescreen.d.ts.map +1 -1
  98. package/dist/utils/index.d.ts +1 -1
  99. package/dist/utils/index.d.ts.map +1 -1
  100. package/dist/utils/patch.d.ts +1 -1
  101. package/dist/utils/patch.d.ts.map +1 -1
  102. package/dist/utils/pro2Nft.d.ts +31 -0
  103. package/dist/utils/pro2Nft.d.ts.map +1 -0
  104. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  105. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  106. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  107. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  108. package/package.json +4 -4
  109. package/src/api/BaseMethod.ts +8 -10
  110. package/src/api/CheckAllFirmwareRelease.ts +40 -53
  111. package/src/api/FirmwareUpdateV2.ts +120 -298
  112. package/src/api/FirmwareUpdateV3.ts +57 -263
  113. package/src/api/FirmwareUpdateV4.ts +307 -912
  114. package/src/api/GetPassphraseState.ts +7 -1
  115. package/src/api/OpenWalletSession.ts +54 -17
  116. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +32 -11
  117. package/src/api/device/DeviceUnlock.ts +1 -1
  118. package/src/api/device/DeviceUpdateBootloader.ts +6 -122
  119. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +0 -57
  120. package/src/api/firmware/updateBootloader.ts +4 -19
  121. package/src/api/firmware/uploadFirmware.ts +22 -140
  122. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  123. package/src/api/index.ts +1 -0
  124. package/src/api/protocol-v2/DeviceUploadNft.ts +189 -0
  125. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  126. package/src/core/deviceEventRegistration.ts +4 -0
  127. package/src/core/index.ts +136 -77
  128. package/src/data/messages/messages-protocol-v2.json +68 -0
  129. package/src/data-manager/DataManager.ts +63 -38
  130. package/src/data-manager/connectSettings.ts +0 -11
  131. package/src/device/Device.ts +203 -21
  132. package/src/events/device.ts +4 -0
  133. package/src/events/logBlockEvent.ts +1 -0
  134. package/src/events/ui-request.ts +33 -2
  135. package/src/index.ts +0 -5
  136. package/src/inject.ts +3 -22
  137. package/src/lowLevelInject.ts +1 -5
  138. package/src/protocols/protocol-v2/features.ts +9 -2
  139. package/src/protocols/protocol-v2/resources.ts +233 -0
  140. package/src/protocols/protocol-v2/uiInteraction.ts +46 -6
  141. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  142. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +124 -0
  143. package/src/protocols/protocol-v2/walletSession.ts +154 -30
  144. package/src/topLevelInject.ts +1 -5
  145. package/src/types/api/checkAllFirmwareRelease.ts +1 -4
  146. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  147. package/src/types/api/export.ts +0 -18
  148. package/src/types/api/firmwareUpdate.ts +0 -76
  149. package/src/types/api/index.ts +2 -14
  150. package/src/types/api/protocolV2.ts +13 -0
  151. package/src/types/settings.ts +26 -13
  152. package/src/utils/deviceFeaturesUtils.ts +4 -0
  153. package/src/utils/homescreen.ts +32 -6
  154. package/src/utils/index.ts +6 -1
  155. package/src/utils/pro2Nft.ts +142 -0
  156. package/src/utils/pro2Wallpaper.ts +35 -8
  157. package/src/utils/uiProgressThrottle.ts +63 -0
  158. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -175
  159. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  160. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  161. package/__tests__/firmware-update/firmware-preparation-error.test.ts +0 -27
  162. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  163. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -593
  164. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -213
  165. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +0 -58
  166. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  167. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  168. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  169. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  170. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  171. package/dist/api/firmware/FirmwarePreparationError.d.ts +0 -3
  172. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +0 -1
  173. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  174. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  175. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -25
  176. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  177. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -30
  178. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  179. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  180. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  181. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  182. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  183. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  184. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  185. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  186. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  187. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -28
  188. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  189. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  190. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  191. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  192. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  193. package/src/api/firmware/FirmwarePreparationError.ts +0 -12
  194. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  195. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -745
  196. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -392
  197. package/src/api/firmware/progressThrottle.ts +0 -44
  198. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  199. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
  200. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  201. package/src/types/api/firmwareUpdatePlan.ts +0 -38
  202. 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,91 @@
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('synthesizes finish metadata for a progress-only operation', () => {
13
+ const device = new Device({ id: 'connect-progress' } as any);
14
+ device.isProtocolV2 = jest.fn(() => true);
15
+ device.beginProtocolV2UiInteraction();
16
+
17
+ const metadata = device.finishProtocolV2UiInteraction('succeeded', {
18
+ ensureMetadata: true,
19
+ });
20
+
21
+ expect(metadata).toMatchObject({
22
+ phase: 'processing',
23
+ transition: 'finish',
24
+ outcome: 'succeeded',
25
+ protocol: 'V2',
26
+ });
27
+ });
28
+
29
+ test('keeps finish metadata after the Protocol V2 transport is released', () => {
30
+ const device = new Device({ id: 'connect-released' } as any);
31
+ const isProtocolV2 = jest.fn().mockReturnValueOnce(true).mockReturnValue(false);
32
+ device.isProtocolV2 = isProtocolV2;
33
+ device.beginProtocolV2UiInteraction();
34
+
35
+ const metadata = device.finishProtocolV2UiInteraction('succeeded', {
36
+ ensureMetadata: true,
37
+ });
38
+
39
+ expect(metadata).toMatchObject({
40
+ phase: 'processing',
41
+ transition: 'finish',
42
+ outcome: 'succeeded',
43
+ protocol: 'V2',
44
+ });
45
+ });
46
+
47
+ test('emits a matching PIN phase completion after DeviceSessionAskPin succeeds', async () => {
48
+ const device = new Device({ id: 'connect-1' } as any);
49
+ device.isProtocolV2 = jest.fn(() => true);
50
+ device.updateProtocolV2Status = jest.fn(() => undefined as any);
51
+ device.commands = {
52
+ typedCall: jest.fn().mockResolvedValue({
53
+ message: { unlocked: true },
54
+ }),
55
+ } as any;
56
+ device.beginProtocolV2UiInteraction();
57
+
58
+ const starts: unknown[] = [];
59
+ const completions: unknown[] = [];
60
+ device.on(DEVICE.PIN_ON_DEVICE, (...args) => starts.push(args));
61
+ device.on(DEVICE.PIN_ON_DEVICE_COMPLETE, (...args) => completions.push(args));
62
+
63
+ await device.unlockDevice(DeviceSessionPinType.Main, {
64
+ source: 'unlock-coordinator',
65
+ reason: 'device-locked',
66
+ deviceOnly: true,
67
+ });
68
+
69
+ const startMetadata = (starts[0] as unknown[])[2] as Record<string, unknown>;
70
+ const completionMetadata = (completions[0] as unknown[])[1] as Record<string, unknown>;
71
+ expect(startMetadata.interaction).toMatchObject({
72
+ phase: 'pin',
73
+ transition: 'start',
74
+ protocol: 'V2',
75
+ });
76
+ expect(completionMetadata).toMatchObject({
77
+ interactionId: (startMetadata.interaction as { interactionId: string }).interactionId,
78
+ phaseId: (startMetadata.interaction as { phaseId: string }).phaseId,
79
+ phase: 'pin',
80
+ transition: 'complete',
81
+ outcome: 'succeeded',
82
+ protocol: 'V2',
83
+ });
84
+ expect(completionMetadata.sequence).toBeGreaterThan(
85
+ (startMetadata.interaction as { sequence: number }).sequence
86
+ );
87
+ expect(completionMetadata.phaseId).toBe(
88
+ (startMetadata.interaction as { phaseId: string }).phaseId
89
+ );
90
+ });
91
+ });
@@ -1,45 +1,206 @@
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');
32
+ });
33
+
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('lets the standard wallet session own the Main PIN unlock', async () => {
65
+ const calls: string[] = [];
66
+ const features = {
67
+ unlocked: false,
68
+ passphraseProtection: true,
69
+ };
70
+ const method = {
71
+ name: 'btcGetPublicKey',
72
+ unlockPolicy: 'none',
73
+ useDevicePassphraseState: true,
74
+ payload: { useEmptyPassphrase: true },
75
+ run: jest.fn().mockImplementation(() => {
76
+ calls.push('run');
77
+ return Promise.resolve({ message: 'ok' });
78
+ }),
79
+ };
80
+ const device = {
81
+ features,
82
+ commands: {
83
+ typedCall: jest.fn().mockImplementation(() => {
84
+ calls.push('status');
85
+ return Promise.resolve({
86
+ message: {
87
+ unlocked: false,
88
+ passphraseProtection: true,
89
+ },
90
+ });
91
+ }),
92
+ },
93
+ isProtocolV2: () => true,
94
+ isBootloader: () => false,
95
+ isRomloader: () => false,
96
+ updateProtocolV2Status: jest.fn(() => features),
97
+ unlockDevice: jest.fn().mockImplementation(() => {
98
+ calls.push('wallet-session-unlock');
99
+ features.unlocked = true;
100
+ return Promise.resolve(features);
101
+ }),
102
+ };
103
+
104
+ await expect(
105
+ runMethodWithUnlockPolicy(method as any, device as any, {
106
+ prepare: async () => {
107
+ await device.unlockDevice();
108
+ },
109
+ })
110
+ ).resolves.toEqual({ message: 'ok' });
111
+
112
+ expect(calls).toEqual(['status', 'wallet-session-unlock', 'run']);
113
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
31
114
  });
32
115
 
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');
116
+ test('reuses a Main PIN selected by pre-unlock when locked status hides passphrase state', async () => {
117
+ const calls: string[] = [];
118
+ const features: {
119
+ unlocked: boolean;
120
+ passphraseProtection?: boolean;
121
+ unlockedAttachPin?: boolean;
122
+ } = {
123
+ unlocked: false,
124
+ };
125
+ const method: any = {
126
+ name: 'btcGetPublicKey',
127
+ unlockPolicy: 'none',
128
+ useDevicePassphraseState: true,
129
+ payload: { useEmptyPassphrase: true },
130
+ run: jest.fn().mockImplementation(() => {
131
+ calls.push('run');
132
+ return Promise.resolve({ message: 'ok' });
133
+ }),
134
+ };
135
+ const device = {
136
+ features,
137
+ commands: {
138
+ typedCall: jest.fn().mockImplementation(() => {
139
+ calls.push('status');
140
+ return Promise.resolve({
141
+ message: {
142
+ unlocked: false,
143
+ },
144
+ });
145
+ }),
146
+ },
147
+ isProtocolV2: () => true,
148
+ isBootloader: () => false,
149
+ isRomloader: () => false,
150
+ updateProtocolV2Status: jest.fn(() => features),
151
+ unlockDevice: jest.fn().mockImplementation(() => {
152
+ calls.push('main-pin');
153
+ features.unlocked = true;
154
+ features.passphraseProtection = true;
155
+ features.unlockedAttachPin = false;
156
+ return Promise.resolve(features);
157
+ }),
158
+ };
159
+
160
+ await expect(
161
+ runMethodWithUnlockPolicy(method, device as any, {
162
+ prepare: async () => {
163
+ if (!(features.unlocked && features.unlockedAttachPin === false)) {
164
+ await device.unlockDevice();
165
+ }
166
+ },
167
+ })
168
+ ).resolves.toEqual({ message: 'ok' });
169
+
170
+ expect(calls).toEqual(['status', 'main-pin', 'run']);
171
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
172
+ });
173
+
174
+ test('runs fresh-status validation before starting unlock', async () => {
175
+ const identityError = new Error('Unexpected device');
176
+ const method = {
177
+ name: 'walletBusinessMethod',
178
+ unlockPolicy: 'none',
179
+ useDevicePassphraseState: true,
180
+ run: jest.fn(),
181
+ };
182
+ const features = { unlocked: false };
183
+ const device = {
184
+ features,
185
+ commands: {
186
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: false } }),
187
+ },
188
+ isProtocolV2: () => true,
189
+ isBootloader: () => false,
190
+ isRomloader: () => false,
191
+ updateProtocolV2Status: jest.fn(() => features),
192
+ unlockDevice: jest.fn(),
193
+ };
194
+
195
+ await expect(
196
+ runMethodWithUnlockPolicy(method as any, device as any, {
197
+ afterStatusBeforeUnlock: () => {
198
+ throw identityError;
199
+ },
200
+ })
201
+ ).rejects.toBe(identityError);
202
+
203
+ expect(device.unlockDevice).not.toHaveBeenCalled();
204
+ expect(method.run).not.toHaveBeenCalled();
44
205
  });
45
206
  });