@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
@@ -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,
@@ -84,7 +84,7 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
84
84
  status: 'required',
85
85
  hasUpgrade: true,
86
86
  required: true,
87
- targetsToUpdate: ['boot', 'app_v1', 'resource'],
87
+ targetsToUpdate: ['boot', 'app_v1'],
88
88
  components: [
89
89
  { configKey: 'bootloader', status: 'outdated', updateTarget: 'boot' },
90
90
  { configKey: 'applicationP1', status: 'outdated', updateTarget: 'app_v1' },
@@ -247,13 +247,26 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
247
247
  firmwareVersion: '1.0.0',
248
248
  },
249
249
  getDeviceState,
250
+ getCommands: () => ({ typedCall: jest.fn().mockResolvedValue({ message: { items: [] } }) }),
250
251
  } as unknown as CheckAllFirmwareRelease['device'];
251
252
  jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
253
+ jest.spyOn(DataManager, 'getProtocolV2Resources').mockReturnValue(
254
+ ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
255
+ (type, index) => ({
256
+ type,
257
+ url: `https://example.com/${type}.okpkg`,
258
+ size: index + 1,
259
+ fileHash: 'a'.repeat(64),
260
+ headerHash: index.toString(16).padStart(128, '0'),
261
+ })
262
+ ) as any
263
+ );
252
264
 
253
265
  await expect(method.run()).resolves.toMatchObject({
254
266
  protocol: 'V2',
255
267
  deviceType: 'pro2',
256
268
  status: 'required',
269
+ resourceStatus: 'outdated',
257
270
  targetsToUpdate: ['boot', 'app_v1', 'resource'],
258
271
  firmware: {
259
272
  status: 'required',
@@ -1,61 +1,19 @@
1
- import DataManager from '../src/data-manager/DataManager';
2
1
  import { parseConnectSettings } from '../src/data-manager/connectSettings';
3
2
 
4
- import type { RemoteConfigResponse } from '../src/types';
5
-
6
- jest.mock('axios', () => ({
7
- get: jest.fn(),
8
- }));
9
-
10
3
  jest.mock('../src/data/config', () => ({
11
4
  getSDKVersion: jest.fn(() => '1.0.0'),
12
5
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
13
6
  }));
14
7
 
15
- const mockAxiosGet: jest.Mock = jest.requireMock('axios').get;
16
-
17
8
  describe('parseConnectSettings', () => {
18
- const preloadedConfig = {
19
- bridge: {},
20
- classic: { firmware: [], ble: [] },
21
- classic1s: { firmware: [], ble: [] },
22
- classicpure: { firmware: [], ble: [] },
23
- mini: { firmware: [], ble: [] },
24
- touch: { firmware: [], ble: [] },
25
- pro: { firmware: [], ble: [] },
26
- pro2: { firmware: [], ble: [] },
27
- } as unknown as RemoteConfigResponse;
28
-
29
- it('keeps a serializable preloaded config in parsed settings', () => {
30
- const settings = parseConnectSettings({
31
- firmwareManifestMode: 'sdk-managed',
32
- preloadedConfig,
33
- });
34
-
35
- expect(settings.firmwareManifestMode).toBe('sdk-managed');
36
- expect(settings.preloadedConfig).toBe(preloadedConfig);
37
- });
38
-
39
- it('uses the preloaded config before the SDK network client', async () => {
40
- const settings = parseConnectSettings({
41
- firmwareManifestMode: 'sdk-managed',
42
- preloadedConfig,
43
- });
44
-
45
- await DataManager.load(settings);
46
-
47
- expect(mockAxiosGet).not.toHaveBeenCalled();
48
- expect(DataManager.deviceMap.classic).toEqual({ firmware: [], ble: [] });
49
- });
9
+ it('keeps custom configFetcher in parsed settings', () => {
10
+ const configFetcher = jest.fn();
50
11
 
51
- it('fails closed without a preloaded config in external-only mode', async () => {
52
12
  const settings = parseConnectSettings({
53
- firmwareManifestMode: 'external-only',
13
+ fetchConfig: true,
14
+ configFetcher,
54
15
  });
55
16
 
56
- await expect(DataManager.load(settings)).rejects.toThrow(
57
- 'External firmware config snapshot is required'
58
- );
59
- expect(mockAxiosGet).not.toHaveBeenCalled();
17
+ expect(settings.configFetcher).toBe(configFetcher);
60
18
  });
61
19
  });
@@ -0,0 +1,272 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
4
+
5
+ jest.mock('../src/data/config', () => ({
6
+ getSDKVersion: jest.fn(() => '1.0.0'),
7
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
8
+ }));
9
+
10
+ const createRgba = (width: number, height: number) => {
11
+ const data = new Uint8Array(width * height * 4);
12
+ for (let index = 3; index < data.length; index += 4) data[index] = 0xff;
13
+ return data;
14
+ };
15
+
16
+ const createMethod = ({
17
+ typedCall,
18
+ supportedMessages = [60805, 60808, 61500],
19
+ useFullBundle = false,
20
+ }: {
21
+ typedCall: jest.Mock;
22
+ supportedMessages?: number[];
23
+ useFullBundle?: boolean;
24
+ }) => {
25
+ const method = new DeviceUploadNft({
26
+ id: 1,
27
+ payload: {
28
+ method: 'deviceUploadNft',
29
+ image: { width: 540, height: 540, rgba: createRgba(540, 540) },
30
+ thumbnail: { width: 263, height: 263, rgba: createRgba(263, 263) },
31
+ title: 'CryptoPunk #3100',
32
+ subtitle: 'CryptoPunks',
33
+ timestampMs: 1_760_000_000_000,
34
+ chunkSize: 2048,
35
+ paceMs: 0,
36
+ },
37
+ });
38
+ (method as any).device = {
39
+ commands: { typedCall },
40
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
41
+ Promise.resolve({
42
+ version: 2,
43
+ build_fingerprint: 'application__1.0.0__test__DEV__DEBUG',
44
+ supported_messages: supportedMessages,
45
+ })
46
+ ),
47
+ getCurrentFirmwareType: jest.fn(),
48
+ };
49
+ method.postMessage = jest.fn();
50
+
51
+ if (useFullBundle) {
52
+ method.init();
53
+ } else {
54
+ (method as any).params = { chunkSize: 2048, paceMs: 0, timeoutMs: 15_000 };
55
+ (method as any).bundle = {
56
+ basename: 'nft-deadbeef-1760000000000',
57
+ image: new Uint8Array([1]),
58
+ thumbnail: new Uint8Array([2]),
59
+ metadata: new TextEncoder().encode('{"title":"NFT","subtitle":""}'),
60
+ };
61
+ }
62
+
63
+ return method;
64
+ };
65
+
66
+ const fileWriteSuccess = (params: { file: { offset: number; data: Uint8Array } }) => ({
67
+ message: {
68
+ processed_byte: params.file.offset + params.file.data.byteLength,
69
+ },
70
+ });
71
+
72
+ const nftFileList = (count: number, basenameAt?: number) =>
73
+ Array.from({ length: count }, (_, index) => {
74
+ const basename =
75
+ index === basenameAt
76
+ ? 'nft-deadbeef-1760000000000'
77
+ : `nft-${index.toString(16).padStart(8, '0')}-${1760000000001 + index}`;
78
+ return [`${basename}.bin`, `${basename}_m.bin`, `${basename}.json`];
79
+ })
80
+ .flat()
81
+ .join('\n');
82
+
83
+ describe('DeviceUploadNft', () => {
84
+ test('uploads the triplet in order without creating the firmware-owned directory', async () => {
85
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
86
+ if (request === 'FilesystemDirList') {
87
+ return { message: { path: 'vol1:/nft', child_files: '' } };
88
+ }
89
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
90
+ if (request === 'NftUpdate') return { message: { message: 'NFT updated' } };
91
+ throw new Error(`Unexpected request: ${request}`);
92
+ });
93
+ const method = createMethod({ typedCall, useFullBundle: true });
94
+
95
+ const result = await method.run();
96
+
97
+ const requests = typedCall.mock.calls.map(call => call[0]);
98
+ expect(requests[0]).toBe('FilesystemDirList');
99
+ expect(typedCall).toHaveBeenNthCalledWith(
100
+ 1,
101
+ 'FilesystemDirList',
102
+ 'FilesystemDir',
103
+ { path: 'vol1:/nft', depth: 1 },
104
+ { timeoutMs: 15_000 }
105
+ );
106
+ expect(requests).not.toContain('FilesystemDirMake');
107
+ expect(requests.at(-1)).toBe('NftUpdate');
108
+ const fileWrites = typedCall.mock.calls.filter(call => call[0] === 'FilesystemFileWrite');
109
+ const paths = fileWrites.map(call => call[2].file.path as string);
110
+ const imagePath = paths[0];
111
+ const thumbnailStart = paths.findIndex(path => path.endsWith('_m.bin'));
112
+ const metadataStart = paths.findIndex(path => path.endsWith('.json'));
113
+ expect(imagePath).toMatch(/^vol1:\/nft\/nft-[a-f0-9]{8}-1760000000000\.bin$/);
114
+ expect(thumbnailStart).toBeGreaterThan(0);
115
+ expect(metadataStart).toBeGreaterThan(thumbnailStart);
116
+ expect(new Set(paths.slice(0, thumbnailStart))).toEqual(new Set([imagePath]));
117
+ expect(new Set(paths.slice(thumbnailStart, metadataStart))).toEqual(
118
+ new Set([result.thumbnailPath])
119
+ );
120
+ expect(typedCall).toHaveBeenLastCalledWith(
121
+ 'NftUpdate',
122
+ 'Success',
123
+ { file_name_no_ext: result.basename },
124
+ { timeoutMs: 15_000 }
125
+ );
126
+ expect(result).toMatchObject({
127
+ nftUpdated: true,
128
+ message: 'NFT updated',
129
+ totalSize: 583_212 + 138_876 + 53,
130
+ });
131
+ expect(method.postMessage).toHaveBeenLastCalledWith({
132
+ event: 'UI_EVENT',
133
+ type: 'ui-device_progress',
134
+ payload: expect.objectContaining({
135
+ progress: 100,
136
+ transferredBytes: result.totalSize,
137
+ totalBytes: result.totalSize,
138
+ }),
139
+ });
140
+ });
141
+
142
+ test('retries one timed-out NftUpdate with the same basename', async () => {
143
+ const timeout = Object.assign(new Error('Protocol V2 response timeout'), {
144
+ code: 'response-timeout',
145
+ });
146
+ let updateCalls = 0;
147
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
148
+ if (request === 'FilesystemDirList') {
149
+ return { message: { path: 'vol1:/nft', child_files: '' } };
150
+ }
151
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
152
+ if (request === 'NftUpdate') {
153
+ updateCalls += 1;
154
+ if (updateCalls === 1) throw timeout;
155
+ return { message: { message: 'NFT updated' } };
156
+ }
157
+ throw new Error(`Unexpected request: ${request}`);
158
+ });
159
+ const method = createMethod({ typedCall });
160
+
161
+ await expect(method.run()).resolves.toMatchObject({ nftUpdated: true });
162
+
163
+ const updateRequests = typedCall.mock.calls.filter(call => call[0] === 'NftUpdate');
164
+ expect(updateRequests).toHaveLength(2);
165
+ expect(updateRequests[1]).toEqual(updateRequests[0]);
166
+ });
167
+
168
+ test('does not retry a non-timeout NftUpdate failure', async () => {
169
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
170
+ if (request === 'FilesystemDirList') {
171
+ return { message: { path: 'vol1:/nft', child_files: '' } };
172
+ }
173
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
174
+ if (request === 'NftUpdate') throw new Error('Invalid NFT metadata');
175
+ throw new Error(`Unexpected request: ${request}`);
176
+ });
177
+ const method = createMethod({ typedCall });
178
+
179
+ await expect(method.run()).rejects.toThrow('Invalid NFT metadata');
180
+ expect(typedCall.mock.calls.filter(call => call[0] === 'NftUpdate')).toHaveLength(1);
181
+ });
182
+
183
+ test('propagates a second NftUpdate timeout after one retry', async () => {
184
+ const timeout = Object.assign(new Error('Protocol V2 response timeout'), {
185
+ code: 'response-timeout',
186
+ });
187
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
188
+ if (request === 'FilesystemDirList') {
189
+ return { message: { path: 'vol1:/nft', child_files: '' } };
190
+ }
191
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
192
+ if (request === 'NftUpdate') throw timeout;
193
+ throw new Error(`Unexpected request: ${request}`);
194
+ });
195
+ const method = createMethod({ typedCall });
196
+
197
+ await expect(method.run()).rejects.toBe(timeout);
198
+ expect(typedCall.mock.calls.filter(call => call[0] === 'NftUpdate')).toHaveLength(2);
199
+ });
200
+
201
+ test('does not publish when a file write fails', async () => {
202
+ const typedCall = jest.fn((request: string) => {
203
+ if (request === 'FilesystemDirList') {
204
+ return { message: { path: 'vol1:/nft', child_files: '' } };
205
+ }
206
+ if (request === 'FilesystemFileWrite') throw new Error('Filesystem full');
207
+ throw new Error(`Unexpected request: ${request}`);
208
+ });
209
+ const method = createMethod({ typedCall });
210
+
211
+ await expect(method.run()).rejects.toThrow('Filesystem full');
212
+ expect(typedCall.mock.calls.some(call => call[0] === 'NftUpdate')).toBe(false);
213
+ });
214
+
215
+ test('rejects unsupported firmware before writing files', async () => {
216
+ const typedCall = jest.fn();
217
+ const method = createMethod({ typedCall, supportedMessages: [60805] });
218
+
219
+ await expect(method.run()).rejects.toBeDefined();
220
+ expect(typedCall).not.toHaveBeenCalled();
221
+ });
222
+
223
+ test('rejects a new NFT before writing when ten complete NFT bundles exist', async () => {
224
+ const typedCall = jest.fn((request: string) => {
225
+ if (request === 'FilesystemDirList') {
226
+ return { message: { path: 'vol1:/nft', child_files: nftFileList(10) } };
227
+ }
228
+ throw new Error(`Unexpected request: ${request}`);
229
+ });
230
+ const method = createMethod({ typedCall });
231
+
232
+ await expect(method.run()).rejects.toMatchObject({
233
+ errorCode: HardwareErrorCode.NftStorageLimitReached,
234
+ params: { count: 10, limit: 10 },
235
+ });
236
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual(['FilesystemDirList']);
237
+ });
238
+
239
+ test('allows an idempotent retry for a basename already counted at the limit', async () => {
240
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
241
+ if (request === 'FilesystemDirList') {
242
+ return { message: { path: 'vol1:/nft', child_files: nftFileList(10, 0) } };
243
+ }
244
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
245
+ if (request === 'NftUpdate') return { message: {} };
246
+ throw new Error(`Unexpected request: ${request}`);
247
+ });
248
+ const method = createMethod({ typedCall });
249
+
250
+ await expect(method.run()).resolves.toMatchObject({ nftUpdated: true });
251
+ expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileWrite')).toBe(true);
252
+ });
253
+
254
+ test('does not count unrelated or incomplete files as stored NFTs', async () => {
255
+ const typedCall = jest.fn((request: string, _response: string, params: any) => {
256
+ if (request === 'FilesystemDirList') {
257
+ return {
258
+ message: {
259
+ path: 'vol1:/nft',
260
+ child_files: `${nftFileList(9)}\nnft-ffffffff-1760000009999.json\nnotes.txt`,
261
+ },
262
+ };
263
+ }
264
+ if (request === 'FilesystemFileWrite') return fileWriteSuccess(params);
265
+ if (request === 'NftUpdate') return { message: {} };
266
+ throw new Error(`Unexpected request: ${request}`);
267
+ });
268
+ const method = createMethod({ typedCall });
269
+
270
+ await expect(method.run()).resolves.toMatchObject({ nftUpdated: true });
271
+ });
272
+ });
@@ -1,8 +1,8 @@
1
- import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import FirmwareUpdateV2 from '../../src/api/FirmwareUpdateV2';
4
4
  import { getBinary } from '../../src/api/firmware/getBinary';
5
- import { uploadFirmwareFromSource } from '../../src/api/firmware/uploadFirmware';
5
+ import { uploadFirmware } from '../../src/api/firmware/uploadFirmware';
6
6
  import * as utils from '../../src/utils';
7
7
 
8
8
  jest.mock('../../src/data/config', () => ({
@@ -18,8 +18,7 @@ jest.mock('../../src/api/firmware/getBinary', () => ({
18
18
 
19
19
  jest.mock('../../src/api/firmware/uploadFirmware', () => ({
20
20
  updateResources: jest.fn(),
21
- updateResourcesFromSources: jest.fn(),
22
- uploadFirmwareFromSource: jest.fn(),
21
+ uploadFirmware: jest.fn(),
23
22
  }));
24
23
 
25
24
  jest.mock('../../src/device/DevicePool', () => ({
@@ -30,9 +29,7 @@ jest.mock('../../src/device/DevicePool', () => ({
30
29
  }));
31
30
 
32
31
  const mockGetBinary = getBinary as jest.MockedFunction<typeof getBinary>;
33
- const mockUploadFirmwareFromSource = uploadFirmwareFromSource as jest.MockedFunction<
34
- typeof uploadFirmwareFromSource
35
- >;
32
+ const mockUploadFirmware = uploadFirmware as jest.MockedFunction<typeof uploadFirmware>;
36
33
 
37
34
  type DeviceType = 'CLASSIC1S' | 'PURE';
38
35
 
@@ -178,26 +175,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
178
175
  expect(mockGetBinary).toHaveBeenCalledTimes(1);
179
176
  expectNoFirmwareMutationCalls(typedCall);
180
177
  expect(acquire).not.toHaveBeenCalled();
181
- expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
182
- });
183
-
184
- it('preserves structured preparation errors before any firmware mutation', async () => {
185
- const preparationError = ERRORS.TypedError(
186
- HardwareErrorCode.RuntimeError,
187
- 'Firmware must be prepared by the external firmware host',
188
- {
189
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
190
- artifactName: 'firmware',
191
- }
192
- );
193
- mockGetBinary.mockRejectedValue(preparationError);
194
- const { method, typedCall, acquire } = createMethod();
195
-
196
- await expect(method.run()).rejects.toBe(preparationError);
197
-
198
- expectNoFirmwareMutationCalls(typedCall);
199
- expect(acquire).not.toHaveBeenCalled();
200
- expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
178
+ expect(mockUploadFirmware).not.toHaveBeenCalled();
201
179
  });
202
180
 
203
181
  it.each([
@@ -238,11 +216,9 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
238
216
  binary: firmwareBinary,
239
217
  } as Awaited<ReturnType<typeof getBinary>>);
240
218
  });
241
- mockUploadFirmwareFromSource.mockImplementation(() => {
219
+ mockUploadFirmware.mockImplementation(() => {
242
220
  callOrder.push('upload');
243
- return Promise.resolve(
244
- expectedResponse as Awaited<ReturnType<typeof uploadFirmwareFromSource>>
245
- );
221
+ return Promise.resolve(expectedResponse as Awaited<ReturnType<typeof uploadFirmware>>);
246
222
  });
247
223
  const { method, typedCall } = createMethod({
248
224
  updateType,
@@ -272,15 +248,15 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
272
248
  },
273
249
  })
274
250
  );
275
- expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
251
+ expect(mockUploadFirmware).toHaveBeenCalledWith(
276
252
  updateType,
277
253
  expect.any(Function),
278
254
  expect.any(Function),
279
255
  method.device,
280
- expect.objectContaining({
281
- size: firmwareBinary.byteLength,
282
- }),
283
- true,
256
+ {
257
+ payload: firmwareBinary,
258
+ rebootOnSuccess: true,
259
+ },
284
260
  isUpdateBootloader
285
261
  );
286
262
  }
@@ -295,11 +271,11 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
295
271
  binary: firmwareBinary,
296
272
  } as Awaited<ReturnType<typeof getBinary>>);
297
273
  });
298
- mockUploadFirmwareFromSource.mockImplementation(() => {
274
+ mockUploadFirmware.mockImplementation(() => {
299
275
  callOrder.push('upload');
300
276
  return Promise.resolve({
301
277
  success: true,
302
- } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
278
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
303
279
  });
304
280
  const { method, typedCall, acquire } = createMethod({
305
281
  bootloaderMode: true,
@@ -324,9 +300,9 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
324
300
  resolveDownload = resolve;
325
301
  })
326
302
  );
327
- mockUploadFirmwareFromSource.mockResolvedValue({
303
+ mockUploadFirmware.mockResolvedValue({
328
304
  success: true,
329
- } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
305
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
330
306
  const { method, typedCall } = createMethod();
331
307
 
332
308
  const runPromise = method.run();
@@ -357,7 +333,7 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
357
333
 
358
334
  expectNoFirmwareMutationCalls(typedCall);
359
335
  expect(acquire).not.toHaveBeenCalled();
360
- expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
336
+ expect(mockUploadFirmware).not.toHaveBeenCalled();
361
337
  });
362
338
 
363
339
  it.each([
@@ -366,23 +342,23 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
366
342
  ])(
367
343
  'keeps the non-empty %s binary overload and never downloads it again',
368
344
  async (_kind, binary) => {
369
- mockUploadFirmwareFromSource.mockResolvedValue({
345
+ mockUploadFirmware.mockResolvedValue({
370
346
  success: true,
371
- } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>);
347
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
372
348
  const { method } = createMethod({ binary });
373
349
 
374
350
  await method.run();
375
351
 
376
352
  expect(mockGetBinary).not.toHaveBeenCalled();
377
- expect(mockUploadFirmwareFromSource).toHaveBeenCalledWith(
353
+ expect(mockUploadFirmware).toHaveBeenCalledWith(
378
354
  'firmware',
379
355
  expect.any(Function),
380
356
  expect.any(Function),
381
357
  method.device,
382
- expect.objectContaining({
383
- size: binary.byteLength,
384
- }),
385
- true,
358
+ {
359
+ payload: binary,
360
+ rebootOnSuccess: true,
361
+ },
386
362
  undefined
387
363
  );
388
364
  }
@@ -393,21 +369,17 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
393
369
  ['DataView', new DataView(new Uint8Array([9, 3, 4, 9]).buffer, 1, 2), [3, 4]],
394
370
  ['custom Buffer', createCustomBuffer(4), [1, 2, 3, 4]],
395
371
  ])('normalizes a non-empty %s before rebooting', async (_kind, binary, expectedBytes) => {
396
- let uploadedBytes: number[] = [];
397
- mockUploadFirmwareFromSource.mockImplementation(
398
- async (_updateType, _typedCall, _postMessage, _device, source) => {
399
- uploadedBytes = Array.from(new Uint8Array(await source.readAt(0, source.size)));
400
- return {
401
- success: true,
402
- } as Awaited<ReturnType<typeof uploadFirmwareFromSource>>;
403
- }
404
- );
372
+ mockUploadFirmware.mockResolvedValue({
373
+ success: true,
374
+ } as Awaited<ReturnType<typeof uploadFirmware>>);
405
375
  const { method } = createMethod({ binary });
406
376
 
407
377
  await method.run();
408
378
 
409
379
  expect(mockGetBinary).not.toHaveBeenCalled();
410
- expect(uploadedBytes).toEqual(expectedBytes);
380
+ const normalizedBinary = mockUploadFirmware.mock.calls[0][4].payload;
381
+ expect(normalizedBinary).toBeInstanceOf(ArrayBuffer);
382
+ expect(Array.from(new Uint8Array(normalizedBinary))).toEqual(expectedBytes);
411
383
  });
412
384
 
413
385
  it.each([
@@ -425,6 +397,6 @@ describe('FirmwareUpdateV2 download-before-reboot safety', () => {
425
397
  expect(mockGetBinary).not.toHaveBeenCalled();
426
398
  expectNoFirmwareMutationCalls(typedCall);
427
399
  expect(acquire).not.toHaveBeenCalled();
428
- expect(mockUploadFirmwareFromSource).not.toHaveBeenCalled();
400
+ expect(mockUploadFirmware).not.toHaveBeenCalled();
429
401
  });
430
402
  });