@onekeyfe/hd-core 1.2.0-alpha.66 → 1.2.0-alpha.67

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 (315) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +0 -313
  2. package/__tests__/DeviceCommands.test.ts +13 -59
  3. package/__tests__/DeviceEventRegistration.test.ts +0 -6
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +3 -198
  5. package/__tests__/connectSettings.test.ts +47 -5
  6. package/__tests__/device-initialize-timeout.test.ts +56 -0
  7. package/__tests__/device-lifecycle-events.test.ts +3 -105
  8. package/__tests__/device-pool-state.test.ts +0 -22
  9. package/__tests__/device-settings.test.ts +18 -39
  10. package/__tests__/device-state-contract.test.ts +0 -1
  11. package/__tests__/device-state-mapper.test.ts +1 -13
  12. package/__tests__/device-utils.test.ts +1 -48
  13. package/__tests__/deviceSettings.test.ts +1 -11
  14. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  15. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  16. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  17. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  18. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  19. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  20. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  21. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  22. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  23. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  24. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  25. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +58 -0
  26. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  27. package/__tests__/get-device-state.test.ts +12 -132
  28. package/__tests__/homescreen.test.ts +1 -41
  29. package/__tests__/kaspa-use-tweak-pro2.test.ts +2 -2
  30. package/__tests__/kaspaSignTransaction.test.ts +26 -3
  31. package/__tests__/logBlockEvent.test.ts +1 -1
  32. package/__tests__/method-protocol-support.test.ts +0 -19
  33. package/__tests__/open-wallet-session.test.ts +37 -568
  34. package/__tests__/protocol-v2-unlock-policy.test.ts +37 -300
  35. package/__tests__/protocol-v2.test.ts +1153 -1458
  36. package/__tests__/protocolV2FileWrite.test.ts +0 -152
  37. package/__tests__/protocolV2UiInteraction.test.ts +0 -63
  38. package/__tests__/public-pro2-api-boundary.test.ts +0 -1
  39. package/__tests__/search-devices.test.ts +3 -12
  40. package/__tests__/tron-sign-message-init.test.ts +2 -2
  41. package/dist/api/BaseMethod.d.ts +1 -3
  42. package/dist/api/BaseMethod.d.ts.map +1 -1
  43. package/dist/api/CheckAllFirmwareRelease.d.ts +1 -2
  44. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  45. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  46. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  47. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  48. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  49. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  50. package/dist/api/FirmwareUpdateV4.d.ts +27 -13
  51. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  52. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  53. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  54. package/dist/api/SearchDevices.d.ts.map +1 -1
  55. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  56. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  57. package/dist/api/device/DeviceUnlock.d.ts +2 -2
  58. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  59. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  60. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  61. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  62. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  63. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  64. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  65. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  66. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  67. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +11 -4
  68. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  69. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  70. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  71. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  72. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  73. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  74. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  75. package/dist/api/firmware/getBinary.d.ts +7 -0
  76. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  77. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  78. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  79. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  80. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  81. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  82. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  83. package/dist/api/helpers/protocolV2FileWrite.d.ts +0 -9
  84. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  85. package/dist/api/index.d.ts +0 -2
  86. package/dist/api/index.d.ts.map +1 -1
  87. package/dist/api/kaspa/KaspaGetAddress.d.ts +1 -1
  88. package/dist/api/kaspa/KaspaSignTransaction.d.ts +1 -1
  89. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  90. package/dist/api/polkadot/PolkadotGetAddress.d.ts +1 -1
  91. package/dist/api/polkadot/networks.d.ts +1 -1
  92. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  93. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  94. package/dist/api/solana/SolSignMessage.d.ts +1 -1
  95. package/dist/api/solana/SolSignOffchainMessage.d.ts +1 -1
  96. package/dist/api/solana/SolSignTransaction.d.ts +2 -2
  97. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  98. package/dist/api/sui/SuiGetAddress.d.ts +1 -1
  99. package/dist/api/sui/SuiGetPublicKey.d.ts +1 -1
  100. package/dist/api/sui/SuiSignMessage.d.ts +1 -1
  101. package/dist/api/sui/SuiSignTransaction.d.ts +1 -1
  102. package/dist/api/ton/TonGetAddress.d.ts +1 -1
  103. package/dist/api/ton/TonSignMessage.d.ts +1 -1
  104. package/dist/api/ton/TonSignProof.d.ts +1 -1
  105. package/dist/api/tron/TronSignMessage.d.ts +2 -2
  106. package/dist/api/tron/TronSignTransaction.d.ts +1 -1
  107. package/dist/core/deviceEventRegistration.d.ts +0 -2
  108. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  109. package/dist/core/index.d.ts.map +1 -1
  110. package/dist/data-manager/DataManager.d.ts +4 -9
  111. package/dist/data-manager/DataManager.d.ts.map +1 -1
  112. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  113. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  114. package/dist/device/Device.d.ts +6 -29
  115. package/dist/device/Device.d.ts.map +1 -1
  116. package/dist/device/DeviceCommands.d.ts +4 -4
  117. package/dist/device/DeviceCommands.d.ts.map +1 -1
  118. package/dist/device/DeviceConnector.d.ts +1 -2
  119. package/dist/device/DeviceConnector.d.ts.map +1 -1
  120. package/dist/device/DevicePool.d.ts.map +1 -1
  121. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -1
  122. package/dist/events/device.d.ts +0 -4
  123. package/dist/events/device.d.ts.map +1 -1
  124. package/dist/events/logBlockEvent.d.ts.map +1 -1
  125. package/dist/events/ui-promise.d.ts +2 -4
  126. package/dist/events/ui-promise.d.ts.map +1 -1
  127. package/dist/events/ui-request.d.ts +2 -31
  128. package/dist/events/ui-request.d.ts.map +1 -1
  129. package/dist/events/ui-response.d.ts +2 -10
  130. package/dist/events/ui-response.d.ts.map +1 -1
  131. package/dist/index.d.ts +247 -243
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +3773 -3054
  134. package/dist/inject.d.ts +1 -6
  135. package/dist/inject.d.ts.map +1 -1
  136. package/dist/lowLevelInject.d.ts.map +1 -1
  137. package/dist/protocols/protocol-v2/features.d.ts +4 -0
  138. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  139. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  140. package/dist/protocols/protocol-v2/lockedError.d.ts +2 -0
  141. package/dist/protocols/protocol-v2/lockedError.d.ts.map +1 -0
  142. package/dist/protocols/protocol-v2/uiInteraction.d.ts +4 -7
  143. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  144. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +3 -1
  145. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  146. package/dist/protocols/protocol-v2/unlockRetry.d.ts +10 -0
  147. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
  148. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -6
  149. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  150. package/dist/topLevelInject.d.ts.map +1 -1
  151. package/dist/types/api/checkAllFirmwareRelease.d.ts +5 -3
  152. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  153. package/dist/types/api/deviceUnlock.d.ts +1 -5
  154. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  155. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  156. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  157. package/dist/types/api/export.d.ts +4 -2
  158. package/dist/types/api/export.d.ts.map +1 -1
  159. package/dist/types/api/firmwareUpdate.d.ts +70 -4
  160. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  161. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  162. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  163. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  164. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  165. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  166. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  167. package/dist/types/api/index.d.ts +10 -7
  168. package/dist/types/api/index.d.ts.map +1 -1
  169. package/dist/types/api/protocolV2.d.ts +0 -3
  170. package/dist/types/api/protocolV2.d.ts.map +1 -1
  171. package/dist/types/device.d.ts +2 -2
  172. package/dist/types/device.d.ts.map +1 -1
  173. package/dist/types/params.d.ts +0 -1
  174. package/dist/types/params.d.ts.map +1 -1
  175. package/dist/types/settings.d.ts +21 -38
  176. package/dist/types/settings.d.ts.map +1 -1
  177. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  178. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  179. package/dist/utils/deviceFeaturesUtils.d.ts +0 -4
  180. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  181. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  182. package/dist/utils/deviceSettings.d.ts.map +1 -1
  183. package/dist/utils/homescreen.d.ts +0 -4
  184. package/dist/utils/homescreen.d.ts.map +1 -1
  185. package/dist/utils/index.d.ts +1 -1
  186. package/dist/utils/index.d.ts.map +1 -1
  187. package/dist/utils/patch.d.ts +1 -1
  188. package/dist/utils/patch.d.ts.map +1 -1
  189. package/dist/utils/pro2Wallpaper.d.ts +0 -10
  190. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  191. package/package.json +4 -4
  192. package/src/api/BaseMethod.ts +10 -8
  193. package/src/api/CheckAllFirmwareRelease.ts +60 -59
  194. package/src/api/FirmwareUpdate.ts +15 -7
  195. package/src/api/FirmwareUpdateV2.ts +298 -126
  196. package/src/api/FirmwareUpdateV3.ts +265 -63
  197. package/src/api/FirmwareUpdateV4.ts +1017 -602
  198. package/src/api/GetPassphraseState.ts +1 -7
  199. package/src/api/OpenWalletSession.ts +17 -71
  200. package/src/api/SearchDevices.ts +1 -3
  201. package/src/api/allnetwork/AllNetworkGetAddress.ts +21 -21
  202. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +11 -36
  203. package/src/api/device/DeviceSettings.ts +4 -5
  204. package/src/api/device/DeviceUnlock.ts +3 -8
  205. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  206. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  207. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  208. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  209. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +83 -27
  210. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  211. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  212. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  213. package/src/api/firmware/progressThrottle.ts +44 -0
  214. package/src/api/firmware/updateBootloader.ts +19 -4
  215. package/src/api/firmware/uploadFirmware.ts +144 -39
  216. package/src/api/helpers/protocolV2FileWrite.ts +63 -202
  217. package/src/api/index.ts +0 -2
  218. package/src/api/kaspa/KaspaGetAddress.ts +1 -1
  219. package/src/api/kaspa/KaspaSignTransaction.ts +13 -2
  220. package/src/api/polkadot/networks.ts +3 -3
  221. package/src/api/protocol-v2/ProtocolInfoRequest.ts +1 -3
  222. package/src/api/protocol-v2/helpers.ts +1 -1
  223. package/src/api/solana/SolSignMessage.ts +1 -1
  224. package/src/api/solana/SolSignOffchainMessage.ts +1 -1
  225. package/src/api/solana/SolSignTransaction.ts +2 -2
  226. package/src/api/sui/SuiGetAddress.ts +1 -1
  227. package/src/api/sui/SuiGetPublicKey.ts +1 -1
  228. package/src/api/sui/SuiSignMessage.ts +1 -1
  229. package/src/api/sui/SuiSignTransaction.ts +1 -1
  230. package/src/api/ton/TonGetAddress.ts +1 -1
  231. package/src/api/ton/TonSignMessage.ts +1 -1
  232. package/src/api/ton/TonSignProof.ts +1 -1
  233. package/src/api/tron/TronSignMessage.ts +2 -2
  234. package/src/api/tron/TronSignTransaction.ts +1 -1
  235. package/src/core/deviceEventRegistration.ts +0 -4
  236. package/src/core/index.ts +111 -213
  237. package/src/data/messages/messages-protocol-v2.json +0 -25
  238. package/src/data-manager/DataManager.ts +41 -113
  239. package/src/data-manager/TransportManager.ts +0 -6
  240. package/src/data-manager/connectSettings.ts +11 -0
  241. package/src/device/Device.ts +49 -274
  242. package/src/device/DeviceCommands.ts +4 -31
  243. package/src/device/DeviceConnector.ts +13 -33
  244. package/src/device/DevicePool.ts +7 -21
  245. package/src/deviceProfile/buildDeviceFeatures.ts +2 -2
  246. package/src/deviceProfile/protocolV2DeviceIdentity.ts +0 -3
  247. package/src/events/device.ts +0 -4
  248. package/src/events/logBlockEvent.ts +0 -1
  249. package/src/events/ui-promise.ts +2 -4
  250. package/src/events/ui-request.ts +2 -36
  251. package/src/events/ui-response.ts +2 -12
  252. package/src/index.ts +5 -0
  253. package/src/inject.ts +24 -60
  254. package/src/lowLevelInject.ts +8 -7
  255. package/src/protocols/protocol-v2/features.ts +7 -10
  256. package/src/protocols/protocol-v2/firmware.ts +0 -2
  257. package/src/protocols/protocol-v2/lockedError.ts +10 -0
  258. package/src/protocols/protocol-v2/uiInteraction.ts +6 -46
  259. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -1
  260. package/src/protocols/protocol-v2/unlockRetry.ts +109 -0
  261. package/src/protocols/protocol-v2/walletSession.ts +36 -214
  262. package/src/topLevelInject.ts +8 -7
  263. package/src/types/api/checkAllFirmwareRelease.ts +5 -3
  264. package/src/types/api/deviceUnlock.ts +1 -12
  265. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  266. package/src/types/api/export.ts +18 -1
  267. package/src/types/api/firmwareUpdate.ts +78 -6
  268. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  269. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  270. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  271. package/src/types/api/index.ts +14 -13
  272. package/src/types/api/protocolV2.ts +0 -13
  273. package/src/types/device.ts +3 -15
  274. package/src/types/params.ts +1 -7
  275. package/src/types/settings.ts +22 -60
  276. package/src/utils/deviceFeaturesCompat.ts +6 -5
  277. package/src/utils/deviceFeaturesUtils.ts +3 -14
  278. package/src/utils/deviceInfoUtils.ts +0 -2
  279. package/src/utils/deviceSettings.ts +0 -3
  280. package/src/utils/homescreen.ts +6 -32
  281. package/src/utils/index.ts +1 -6
  282. package/src/utils/pro2Wallpaper.ts +8 -35
  283. package/__tests__/core-initialization.test.ts +0 -23
  284. package/__tests__/device-connector-protocol.test.ts +0 -81
  285. package/__tests__/deviceUploadNft.test.ts +0 -316
  286. package/__tests__/pro2Nft.test.ts +0 -108
  287. package/__tests__/protocol-binding.test.ts +0 -89
  288. package/__tests__/protocol-v2-resources.test.ts +0 -340
  289. package/__tests__/protocol-v2-ui-lifecycle.test.ts +0 -91
  290. package/__tests__/uiProgressThrottle.test.ts +0 -74
  291. package/__tests__/uiPromiseRegistry.test.ts +0 -115
  292. package/dist/api/DetectDeviceConnectProtocol.d.ts +0 -7
  293. package/dist/api/DetectDeviceConnectProtocol.d.ts.map +0 -1
  294. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +0 -31
  295. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +0 -1
  296. package/dist/core/uiPromiseRegistry.d.ts +0 -6
  297. package/dist/core/uiPromiseRegistry.d.ts.map +0 -1
  298. package/dist/protocols/protocol-v2/resources.d.ts +0 -30
  299. package/dist/protocols/protocol-v2/resources.d.ts.map +0 -1
  300. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +0 -22
  301. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +0 -1
  302. package/dist/types/api/detectDeviceConnectProtocol.d.ts +0 -4
  303. package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +0 -1
  304. package/dist/utils/pro2Nft.d.ts +0 -31
  305. package/dist/utils/pro2Nft.d.ts.map +0 -1
  306. package/dist/utils/uiProgressThrottle.d.ts +0 -3
  307. package/dist/utils/uiProgressThrottle.d.ts.map +0 -1
  308. package/src/api/DetectDeviceConnectProtocol.ts +0 -18
  309. package/src/api/protocol-v2/DeviceUploadNft.ts +0 -197
  310. package/src/core/uiPromiseRegistry.ts +0 -63
  311. package/src/protocols/protocol-v2/resources.ts +0 -390
  312. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +0 -131
  313. package/src/types/api/detectDeviceConnectProtocol.ts +0 -6
  314. package/src/utils/pro2Nft.ts +0 -142
  315. package/src/utils/uiProgressThrottle.ts +0 -63
@@ -1,11 +1,10 @@
1
- import { EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import {
3
3
  DeviceRebootType,
4
4
  DeviceSessionPinType,
5
5
  DeviceSettingsPage,
6
6
  DeviceType,
7
7
  } from '@onekeyfe/hd-transport';
8
- import { sha256 } from '@noble/hashes/sha256';
9
8
 
10
9
  import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
11
10
  import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
@@ -62,7 +61,6 @@ import { createCoreApi } from '../src/inject';
62
61
  import { Device, preloadSessionCache } from '../src/device/Device';
63
62
  import { deviceWalletSessionStore } from '../src/device/DeviceWalletSessionStore';
64
63
  import { DevicePool } from '../src/device/DevicePool';
65
- import { DEVICE } from '../src/events';
66
64
  import { UI_REQUEST } from '../src/events/ui-request';
67
65
  import {
68
66
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
@@ -76,15 +74,11 @@ import {
76
74
  requestProtocolV2ProtocolInfo,
77
75
  supportsProtocolV2Message,
78
76
  } from '../src/protocols/protocol-v2/features';
79
- import { PROTOCOL_V2_RESOURCE_DEVICE_PATHS } from '../src/protocols/protocol-v2/resources';
80
77
  import {
81
78
  getProtocolV2WalletSession,
82
79
  refreshProtocolV2DeviceStatus,
83
80
  } from '../src/protocols/protocol-v2/walletSession';
84
- import {
85
- createProtocolV2UnlockContext,
86
- runMethodWithUnlockPolicy,
87
- } from '../src/protocols/protocol-v2/unlockPolicyRunner';
81
+ import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry';
88
82
  import { BaseMethod } from '../src/api/BaseMethod';
89
83
  import {
90
84
  buildProtocolV1FeaturesPayload,
@@ -98,6 +92,7 @@ import {
98
92
  getMethodVersionRange,
99
93
  } from '../src/utils';
100
94
  import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
95
+ import { openFirmwareByteSource } from '../src/api/firmware/FirmwareArtifactSource';
101
96
  import {
102
97
  getPassphraseState,
103
98
  getPassphraseStateWithRefreshDeviceInfo,
@@ -246,7 +241,7 @@ describe('UploadPortfolio', () => {
246
241
  (method as any).device = device;
247
242
 
248
243
  method.init();
249
- await runMethodWithUnlockPolicy(method, device as any);
244
+ await runMethodWithUnlockRetry(method, device as any);
250
245
 
251
246
  expect(unlockDevice).not.toHaveBeenCalled();
252
247
  expect(typedCall).toHaveBeenNthCalledWith(
@@ -422,16 +417,14 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
422
417
  }
423
418
  return d.features;
424
419
  };
425
- d.ensureProtocolV2RuntimeContext ??= (timeoutMs?: number) =>
426
- requestProtocolV2ProtocolInfo({
427
- commands: d.getCommands?.() ?? d.commands,
428
- timeoutMs,
429
- });
430
420
  d.probeProtocolV2RuntimeState ??= async (
431
421
  deviceInfo: ProtocolV2DeviceInfo,
432
422
  timeoutMs?: number
433
423
  ) => {
434
- const protocolInfo = await d.ensureProtocolV2RuntimeContext(timeoutMs);
424
+ const protocolInfo = await requestProtocolV2ProtocolInfo({
425
+ commands: d.getCommands(),
426
+ timeoutMs,
427
+ });
435
428
  const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
436
429
  if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
437
430
  return d.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
@@ -589,7 +582,7 @@ describe('Protocol V2 feature adapter', () => {
589
582
  } as any);
590
583
  const protocolV2 = buildProtocolV2FeaturesPayload({
591
584
  deviceInfo: {
592
- hw: { Device_type: DeviceType.PRO2, serial_no: 'P2-001' },
585
+ hw: { serial_no: 'P2-001' },
593
586
  coprocessor: { bt_adv_name: 'Pro 2 BLE' },
594
587
  },
595
588
  deviceStatus: {
@@ -788,9 +781,6 @@ describe('Protocol V2 feature adapter', () => {
788
781
  if (request === 'DeviceStatusGet') {
789
782
  return { type: 'DeviceStatus', message: { unlocked: true } };
790
783
  }
791
- if (request === 'DeviceSessionAskPassphrase') {
792
- return { type: 'Success', message: {} };
793
- }
794
784
  if (request === 'DeviceSessionGet') {
795
785
  return {
796
786
  type: 'DeviceSession',
@@ -815,7 +805,7 @@ describe('Protocol V2 feature adapter', () => {
815
805
  ).resolves.toEqual({
816
806
  passphraseState: 'state-1',
817
807
  newSession: 'session-1',
818
- unlockedAttachPin: false,
808
+ unlockedAttachPin: undefined,
819
809
  resumed: false,
820
810
  });
821
811
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
@@ -824,13 +814,7 @@ describe('Protocol V2 feature adapter', () => {
824
814
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', {
825
815
  type: DeviceSessionPinType.Main,
826
816
  });
827
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
828
- passphrase: '',
829
- on_device: false,
830
- });
831
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
832
- seed_domains: [],
833
- });
817
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
834
818
  device.passphraseState = 'state-1';
835
819
  expect(device.getInternalState()).toBe('session-1');
836
820
  });
@@ -863,7 +847,6 @@ describe('Protocol V2 feature adapter', () => {
863
847
  },
864
848
  };
865
849
  }
866
- if (request === 'DeviceSessionAskPassphrase') return { message: {} };
867
850
  if (request === 'DeviceSessionGet') {
868
851
  return {
869
852
  message: {
@@ -880,16 +863,9 @@ describe('Protocol V2 feature adapter', () => {
880
863
  await getProtocolV2WalletSession(device, { onlyMainPin: true });
881
864
 
882
865
  expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPin')).toHaveLength(1);
883
- expect(
884
- typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionAskPassphrase')
885
- ).toHaveLength(1);
886
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
887
- seed_domains: [],
888
- });
866
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
889
867
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
890
868
  session_id: 'standard-session',
891
- btc_test_address: 'standard-state',
892
- seed_domains: [],
893
869
  });
894
870
  expect(deviceWalletSessionStore.getStandard(deviceId)).toEqual({
895
871
  passphraseState: 'standard-state',
@@ -931,7 +907,6 @@ describe('Protocol V2 feature adapter', () => {
931
907
  },
932
908
  };
933
909
  }
934
- if (request === 'DeviceSessionAskPassphrase') return { message: {} };
935
910
  if (request === 'DeviceSessionAskPin') {
936
911
  return { type: 'Success', message: { message: 'PIN verified' } };
937
912
  }
@@ -1026,7 +1001,6 @@ describe('Protocol V2 feature adapter', () => {
1026
1001
  if (request === 'DeviceStatusGet') {
1027
1002
  return { message: { device_id: deviceId, unlocked: true, passphrase_enabled: true } };
1028
1003
  }
1029
- if (request === 'DeviceSessionAskPassphrase') return { message: {} };
1030
1004
  if (request === 'DeviceSessionGet') {
1031
1005
  sessionGetCalls += 1;
1032
1006
  return {
@@ -1253,9 +1227,7 @@ describe('Protocol V2 feature adapter', () => {
1253
1227
  passphrase: 'host hidden wallet',
1254
1228
  on_device: false,
1255
1229
  });
1256
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1257
- seed_domains: [],
1258
- });
1230
+ expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {});
1259
1231
  });
1260
1232
 
1261
1233
  test('deviceStatusGet returns raw DeviceStatus and updates dynamic features', async () => {
@@ -1363,8 +1335,6 @@ describe('Protocol V2 feature adapter', () => {
1363
1335
 
1364
1336
  expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1365
1337
  session_id: 'session-a',
1366
- btc_test_address: 'state-a',
1367
- seed_domains: [],
1368
1338
  });
1369
1339
  expect(device.getInternalState()).toBe('session-b');
1370
1340
  });
@@ -1432,15 +1402,7 @@ describe('Protocol V2 feature adapter', () => {
1432
1402
  });
1433
1403
  expect(typedCall.mock.calls).toEqual([
1434
1404
  ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
1435
- [
1436
- 'DeviceSessionGet',
1437
- 'DeviceSession',
1438
- {
1439
- session_id: 'session-a',
1440
- btc_test_address: 'state-a',
1441
- seed_domains: [],
1442
- },
1443
- ],
1405
+ ['DeviceSessionGet', 'DeviceSession', { session_id: 'session-a' }],
1444
1406
  ]);
1445
1407
  expect(promptPassphrase).not.toHaveBeenCalled();
1446
1408
  expect(device.getInternalState()).toBeUndefined();
@@ -1488,7 +1450,7 @@ describe('Protocol V2 feature adapter', () => {
1488
1450
  { passphrase: 'host hidden wallet', on_device: false },
1489
1451
  ],
1490
1452
  ['DeviceStatusGet', 'DeviceStatus', {}],
1491
- ['DeviceSessionGet', 'DeviceSession', { seed_domains: [] }],
1453
+ ['DeviceSessionGet', 'DeviceSession', {}],
1492
1454
  ]);
1493
1455
  });
1494
1456
 
@@ -1591,73 +1553,6 @@ describe('Protocol V2 feature adapter', () => {
1591
1553
  expect(device.getInternalState()).toBeUndefined();
1592
1554
  });
1593
1555
 
1594
- test.each([false, true])(
1595
- 'rejects a different Attach PIN wallet when Pro2 lock failure is %s',
1596
- async lockFails => {
1597
- const deviceId = 'attach-mismatch-device';
1598
- const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1599
- (device as any).features = normalizeProtocolV2Features(
1600
- { ...descriptor, protocolType: 'V2' } as any,
1601
- {
1602
- status: {
1603
- device_id: deviceId,
1604
- unlocked: true,
1605
- passphrase_enabled: true,
1606
- attach_to_pin_enabled: true,
1607
- unlocked_by_attach_to_pin: false,
1608
- },
1609
- }
1610
- );
1611
- device.passphraseState = 'expected-state';
1612
-
1613
- const typedCall = jest.fn((request: string) => {
1614
- if (request === 'ProtocolInfoRequest') return { message: { version: 2 } };
1615
- if (request === 'DeviceSessionAskPin') return { message: { message: 'PIN verified' } };
1616
- if (request === 'DeviceStatusGet') {
1617
- return {
1618
- message: {
1619
- device_id: deviceId,
1620
- unlocked: true,
1621
- passphrase_enabled: true,
1622
- attach_to_pin_enabled: true,
1623
- unlocked_by_attach_to_pin: true,
1624
- },
1625
- };
1626
- }
1627
- if (request === 'DeviceSessionGet') {
1628
- return {
1629
- message: {
1630
- btc_test_address: 'wrong-attach-state',
1631
- session_id: 'wrong-attach-session',
1632
- },
1633
- };
1634
- }
1635
- if (request === 'LockDevice') {
1636
- if (lockFails) throw new Error('Lock unsupported');
1637
- return { message: { message: 'Device locked' } };
1638
- }
1639
- throw new Error(`Unexpected request: ${request}`);
1640
- });
1641
- const promptPassphrase = jest.fn().mockResolvedValue({ attachPinOnDevice: true });
1642
- (device as any).commands = { typedCall, promptPassphrase };
1643
-
1644
- await expect(
1645
- getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
1646
- ).rejects.toMatchObject({
1647
- errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
1648
- });
1649
-
1650
- expect(promptPassphrase).not.toHaveBeenCalled();
1651
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
1652
- btc_test_address: 'expected-state',
1653
- seed_domains: [],
1654
- });
1655
- expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
1656
- expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toHaveLength(1);
1657
- expect(device.getInternalState()).toBeUndefined();
1658
- }
1659
- );
1660
-
1661
1556
  test('recovers an expired Attach PIN wallet through the unified wallet selector', async () => {
1662
1557
  const device = Device.fromDescriptor({
1663
1558
  id: 'cache-device-attach-recovery',
@@ -1714,17 +1609,7 @@ describe('Protocol V2 feature adapter', () => {
1714
1609
  }),
1715
1610
  { cancelDeviceOnReject: false }
1716
1611
  );
1717
- expect(unlockDevice).toHaveBeenCalledWith(
1718
- DeviceSessionPinType.AttachToPin,
1719
- expect.objectContaining({
1720
- emitUiEvent: false,
1721
- interaction: expect.objectContaining({
1722
- phase: 'pin',
1723
- transition: 'start',
1724
- protocol: 'V2',
1725
- }),
1726
- })
1727
- );
1612
+ expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.AttachToPin);
1728
1613
  expect(device.getInternalState()).toBe('new-attach-session');
1729
1614
  });
1730
1615
 
@@ -2008,10 +1893,12 @@ describe('Protocol V2 feature adapter', () => {
2008
1893
  expect(getFeatures).not.toHaveBeenCalled();
2009
1894
  });
2010
1895
 
2011
- test('uses the cached wallet address when the local session is missing', async () => {
1896
+ test('marks an interactive cache miss as a wallet-session recovery', async () => {
2012
1897
  const typedCall = jest
2013
1898
  .fn()
2014
1899
  .mockResolvedValueOnce({ message: { version: 2 } })
1900
+ .mockResolvedValueOnce({ message: { message: 'Passphrase prepared' } })
1901
+ .mockResolvedValueOnce({ message: { unlocked: true, unlocked_attach_pin: false } })
2015
1902
  .mockResolvedValueOnce({
2016
1903
  message: {
2017
1904
  btc_test_address: 'expected-state',
@@ -2039,11 +1926,16 @@ describe('Protocol V2 feature adapter', () => {
2039
1926
  await expect(
2040
1927
  getProtocolV2WalletSession(device, { expectedPassphraseState: 'expected-state' })
2041
1928
  ).resolves.toMatchObject({ passphraseState: 'expected-state' });
2042
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2043
- btc_test_address: 'expected-state',
2044
- seed_domains: [],
2045
- });
2046
- expect(promptPassphrase).not.toHaveBeenCalled();
1929
+ expect(promptPassphrase).toHaveBeenCalledWith(
1930
+ {
1931
+ existsAttachPinUser: false,
1932
+ deviceOnly: false,
1933
+ source: 'wallet-session-coordinator',
1934
+ reason: 'session-recovery',
1935
+ expectedPassphraseState: 'expected-state',
1936
+ },
1937
+ { cancelDeviceOnReject: false }
1938
+ );
2047
1939
  });
2048
1940
 
2049
1941
  test('reuses the Pro2 session opened by legacy getPassphraseState on the next App call', async () => {
@@ -2109,15 +2001,8 @@ describe('Protocol V2 feature adapter', () => {
2109
2001
  true
2110
2002
  );
2111
2003
  expect(typedCall.mock.calls).toEqual([
2112
- [
2113
- 'DeviceSessionGet',
2114
- 'DeviceSession',
2115
- {
2116
- session_id: 'session-pro2-app',
2117
- btc_test_address: 'state-pro2-app',
2118
- seed_domains: [],
2119
- },
2120
- ],
2004
+ ['ProtocolInfoRequest', 'ProtocolInfo', { eventless_wallet_session: true }],
2005
+ ['DeviceSessionGet', 'DeviceSession', { session_id: 'session-pro2-app' }],
2121
2006
  ]);
2122
2007
  });
2123
2008
 
@@ -2306,9 +2191,7 @@ describe('Protocol V2 feature adapter', () => {
2306
2191
  passphrase: 'host hidden wallet',
2307
2192
  on_device: false,
2308
2193
  });
2309
- expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {
2310
- seed_domains: [],
2311
- });
2194
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
2312
2195
  });
2313
2196
 
2314
2197
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
@@ -2386,83 +2269,12 @@ describe('Protocol V2 feature adapter', () => {
2386
2269
  );
2387
2270
 
2388
2271
  expect(device.getInternalState()).toBeUndefined();
2389
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2390
- btc_test_address: 'expected-state',
2391
- seed_domains: [],
2392
- });
2393
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2272
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'DeviceSessionAskPassphrase', 'Success', {
2394
2273
  passphrase: 'host hidden wallet',
2395
2274
  on_device: false,
2396
2275
  });
2397
- expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2398
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2399
- seed_domains: [],
2400
- });
2401
- });
2402
-
2403
- test('fails closed instead of switching to Main PIN during a standard-wallet safety check', async () => {
2404
- const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
2405
- const typedCall = jest.fn((request: string) => {
2406
- if (request === 'ProtocolInfoRequest') {
2407
- return { message: { version: 2 } };
2408
- }
2409
- if (request === 'DeviceStatusGet') {
2410
- return {
2411
- message: {
2412
- device_id: 'attach-wallet-device',
2413
- unlocked: true,
2414
- passphrase_enabled: true,
2415
- attach_to_pin_enabled: true,
2416
- unlocked_by_attach_to_pin: true,
2417
- },
2418
- };
2419
- }
2420
- if (request === 'DeviceSessionAskPin' || request === 'DeviceSessionAskPassphrase') {
2421
- return { message: { message: 'Wallet selected' } };
2422
- }
2423
- if (request === 'DeviceSessionGet') {
2424
- return {
2425
- message: {
2426
- session_id: 'main-wallet-session',
2427
- btc_test_address: 'main-wallet-state',
2428
- },
2429
- };
2430
- }
2431
- if (request === 'LockDevice') {
2432
- return { message: { message: 'Device locked' } };
2433
- }
2434
- throw new Error(`Unexpected request: ${request}`);
2435
- });
2436
-
2437
- (device as any).features = normalizeProtocolV2Features(
2438
- { ...descriptor, protocolType: 'V2' } as any,
2439
- {
2440
- status: {
2441
- device_id: 'attach-wallet-device',
2442
- unlocked: true,
2443
- passphrase_enabled: true,
2444
- attach_to_pin_enabled: true,
2445
- // 模拟缓存滞后:设备状态刷新后才确认当前由 Attach PIN 解锁。
2446
- unlocked_by_attach_to_pin: false,
2447
- },
2448
- }
2449
- );
2450
- (device as any).commands = { typedCall };
2451
- const clearInternalState = jest.spyOn(device, 'clearInternalState');
2452
-
2453
- await expect(device.checkPassphraseStateSafety(undefined, true, false)).rejects.toMatchObject({
2454
- errorCode: HardwareErrorCode.DeviceCheckUnlockTypeError,
2455
- });
2456
-
2457
- expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2458
- expect(typedCall).toHaveBeenCalledWith('LockDevice', 'Success', {});
2459
- expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
2460
- expect(typedCall).not.toHaveBeenCalledWith(
2461
- 'DeviceSessionAskPassphrase',
2462
- 'Success',
2463
- expect.anything()
2464
- );
2465
- expect(clearInternalState).toHaveBeenCalledTimes(1);
2276
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
2277
+ expect(typedCall).toHaveBeenNthCalledWith(4, 'DeviceSessionGet', 'DeviceSession', {});
2466
2278
  });
2467
2279
 
2468
2280
  test('useEmptyPassphrase ignores a stale hidden-wallet state during Protocol V2 safety check', async () => {
@@ -2495,22 +2307,10 @@ describe('Protocol V2 feature adapter', () => {
2495
2307
  await expect(
2496
2308
  device.checkPassphraseStateSafety('stale-hidden-state', true, false)
2497
2309
  ).resolves.toBe(true);
2498
- expect(typedCall).toHaveBeenCalledTimes(5);
2310
+ expect(typedCall).toHaveBeenCalledTimes(4);
2499
2311
  expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
2500
2312
  eventless_wallet_session: true,
2501
2313
  });
2502
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {
2503
- seed_domains: [],
2504
- });
2505
- expect(typedCall).toHaveBeenCalledWith('DeviceSessionAskPassphrase', 'Success', {
2506
- passphrase: '',
2507
- on_device: false,
2508
- });
2509
- expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2510
- expect(typedCall.mock.calls.filter(([request]) => request === 'DeviceStatusGet')).toHaveLength(
2511
- 2
2512
- );
2513
- expect(typedCall).not.toHaveBeenCalledWith('DeviceSessionAskPin', 'Success', expect.anything());
2514
2314
  });
2515
2315
 
2516
2316
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
@@ -2581,7 +2381,7 @@ describe('Protocol V2 feature adapter', () => {
2581
2381
  uuid: 'CACHED-SERIAL',
2582
2382
  deviceId: null,
2583
2383
  bleName: 'Cached BLE',
2584
- name: 'Cached BLE',
2384
+ name: 'Cached Label',
2585
2385
  label: 'Cached Label',
2586
2386
  deviceType: 'pro2',
2587
2387
  });
@@ -2608,7 +2408,7 @@ describe('Protocol V2 feature adapter', () => {
2608
2408
  expect(device.features?.label).toBeNull();
2609
2409
  });
2610
2410
 
2611
- test('uses the BLE name for discovery while preserving the Protocol V2 label', () => {
2411
+ test('prefers the real Protocol V2 label over the BLE name for display', () => {
2612
2412
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
2613
2413
  (device as any).features = {
2614
2414
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
@@ -2616,10 +2416,9 @@ describe('Protocol V2 feature adapter', () => {
2616
2416
  bleName: 'Pro2 6136',
2617
2417
  };
2618
2418
 
2619
- expect(device.getCurrentDisplayName()).toBe('My Pro 2');
2620
2419
  expect(device.toMessageObject()).toMatchObject({
2621
2420
  bleName: 'Pro2 6136',
2622
- name: 'Pro2 6136',
2421
+ name: 'My Pro 2',
2623
2422
  label: 'My Pro 2',
2624
2423
  });
2625
2424
  });
@@ -2673,6 +2472,10 @@ describe('Protocol V2 feature adapter', () => {
2673
2472
  hw: true,
2674
2473
  fw: true,
2675
2474
  coprocessor: true,
2475
+ se1: true,
2476
+ se2: true,
2477
+ se3: true,
2478
+ se4: true,
2676
2479
  },
2677
2480
  types: {
2678
2481
  version: true,
@@ -2696,10 +2499,8 @@ describe('Protocol V2 feature adapter', () => {
2696
2499
  ).rejects.toThrow('DeviceInfo not supported');
2697
2500
  });
2698
2501
 
2699
- test('does not inherit Pro or model_touch ranges for Protocol V2 devices', () => {
2700
- const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
2701
- hw: { Device_type: DeviceType.PRO2 },
2702
- });
2502
+ test('does not inherit Pro or Pro model fallback ranges for Protocol V2 devices', () => {
2503
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
2703
2504
  const checkedTypes: string[] = [];
2704
2505
 
2705
2506
  const versionRange = getMethodVersionRange(features, type => {
@@ -2711,13 +2512,11 @@ describe('Protocol V2 feature adapter', () => {
2711
2512
  });
2712
2513
 
2713
2514
  expect(versionRange).toBeUndefined();
2714
- expect(checkedTypes).toEqual(['pro2', 'model_pro2']);
2515
+ expect(checkedTypes).toEqual(['pro2']);
2715
2516
  });
2716
2517
 
2717
2518
  test('uses firmware-v1 as the Pro2 remote firmware config field', () => {
2718
- const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
2719
- hw: { Device_type: DeviceType.PRO2 },
2720
- });
2519
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
2721
2520
 
2722
2521
  expect(
2723
2522
  getFirmwareUpdateField({
@@ -2868,6 +2667,10 @@ describe('Protocol V2 feature adapter', () => {
2868
2667
  hw: true,
2869
2668
  fw: true,
2870
2669
  coprocessor: true,
2670
+ se1: true,
2671
+ se2: true,
2672
+ se3: true,
2673
+ se4: true,
2871
2674
  },
2872
2675
  types: {
2873
2676
  version: true,
@@ -2878,9 +2681,7 @@ describe('Protocol V2 feature adapter', () => {
2878
2681
  timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
2879
2682
  }
2880
2683
  );
2881
- expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
2882
- eventless_wallet_session: true,
2883
- });
2684
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
2884
2685
  expect(typedCall).toHaveBeenNthCalledWith(3, 'DeviceStatusGet', 'DeviceStatus', {});
2885
2686
  });
2886
2687
 
@@ -2918,9 +2719,7 @@ describe('Protocol V2 feature adapter', () => {
2918
2719
  }),
2919
2720
  expect.anything()
2920
2721
  );
2921
- expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {
2922
- eventless_wallet_session: true,
2923
- });
2722
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'ProtocolInfoRequest', 'ProtocolInfo', {});
2924
2723
  expect(typedCall).not.toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
2925
2724
  expect(device.features).toMatchObject({
2926
2725
  deviceId: null,
@@ -3186,6 +2985,10 @@ describe('Protocol V2 feature adapter', () => {
3186
2985
  fw: { application: { version: '1.2.4' } },
3187
2986
  },
3188
2987
  })
2988
+ .mockResolvedValueOnce({
2989
+ type: 'ProtocolInfo',
2990
+ message: protocolV2ApplicationInfo,
2991
+ })
3189
2992
  .mockResolvedValueOnce({
3190
2993
  type: 'DeviceStatus',
3191
2994
  message: { init_states: true, unlocked: true },
@@ -3207,7 +3010,7 @@ describe('Protocol V2 feature adapter', () => {
3207
3010
  firmwareVersion: '1.2.4',
3208
3011
  passphraseProtection: null,
3209
3012
  });
3210
- expect(typedCall).toHaveBeenCalledTimes(5);
3013
+ expect(typedCall).toHaveBeenCalledTimes(6);
3211
3014
  expect(typedCall).toHaveBeenNthCalledWith(
3212
3015
  4,
3213
3016
  'DeviceInfoGet',
@@ -3217,6 +3020,10 @@ describe('Protocol V2 feature adapter', () => {
3217
3020
  hw: true,
3218
3021
  fw: true,
3219
3022
  coprocessor: true,
3023
+ se1: true,
3024
+ se2: true,
3025
+ se3: true,
3026
+ se4: true,
3220
3027
  },
3221
3028
  types: {
3222
3029
  version: true,
@@ -3317,23 +3124,9 @@ describe('Protocol V2 feature adapter', () => {
3317
3124
  status: { init_states: true },
3318
3125
  }
3319
3126
  );
3320
- const pinOnDevice = jest.fn();
3321
- device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
3322
3127
 
3323
3128
  const features = await device.unlockDevice();
3324
3129
 
3325
- expect(pinOnDevice).toHaveBeenCalledWith(
3326
- device,
3327
- DeviceSessionPinType.Main,
3328
- expect.objectContaining({
3329
- source: 'unlock-coordinator',
3330
- reason: 'device-unlock',
3331
- deviceOnly: true,
3332
- })
3333
- );
3334
- expect(pinOnDevice.mock.invocationCallOrder[0]).toBeLessThan(
3335
- typedCall.mock.invocationCallOrder[0]
3336
- );
3337
3130
  expect(typedCall.mock.calls).toEqual([
3338
3131
  ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
3339
3132
  ['DeviceStatusGet', 'DeviceStatus', {}],
@@ -3379,12 +3172,9 @@ describe('Protocol V2 feature adapter', () => {
3379
3172
  throw new Error(`Unexpected request: ${requestType}`);
3380
3173
  });
3381
3174
  (device as any).commands = { typedCall };
3382
- const pinOnDevice = jest.fn();
3383
- device.on(DEVICE.PIN_ON_DEVICE, pinOnDevice);
3384
3175
 
3385
- await device.unlockDevice(DeviceSessionPinType.Main, { emitUiEvent: false });
3176
+ await device.unlockDevice();
3386
3177
 
3387
- expect(pinOnDevice).not.toHaveBeenCalled();
3388
3178
  expect(typedCall.mock.calls).toEqual([
3389
3179
  ['DeviceSessionAskPin', 'Success', { type: DeviceSessionPinType.Main }],
3390
3180
  ['DeviceStatusGet', 'DeviceStatus', {}],
@@ -3627,77 +3417,71 @@ describe('API compatibility handling', () => {
3627
3417
  });
3628
3418
  });
3629
3419
 
3630
- test.each(['pro2', 'neo'] as const)(
3631
- 'allows %s Solana signing methods through Protocol V2 version checks',
3632
- deviceType => {
3633
- const features = {
3634
- deviceType,
3635
- } as Features;
3420
+ test('allows Pro2 Solana signing methods through Protocol V2 version checks', () => {
3421
+ const features = {
3422
+ deviceType: 'pro2',
3423
+ } as Features;
3636
3424
 
3637
- const solSignMessageMethod = new SolSignMessage({
3638
- id: 1,
3639
- payload: {
3640
- method: 'solSignMessage',
3641
- path: "m/44'/501'/0'/0'",
3642
- messageHex: '48656c6c6f',
3643
- },
3644
- });
3645
- const solSignOffchainMessageMethod = new SolSignOffchainMessage({
3646
- id: 2,
3647
- payload: {
3648
- method: 'solSignOffchainMessage',
3649
- path: "m/44'/501'/0'/0'",
3650
- messageHex: '48656c6c6f',
3651
- },
3652
- });
3653
- const solSignTransactionMethod = new SolSignTransaction({
3654
- id: 3,
3655
- payload: {
3656
- method: 'solSignTransaction',
3657
- path: "m/44'/501'/0'/0'",
3658
- rawTx: '00',
3659
- },
3660
- });
3661
- const solSignVersionedTransactionMethod = new SolSignTransaction({
3662
- id: 4,
3663
- payload: {
3664
- method: 'solSignTransaction',
3665
- path: "m/44'/501'/0'/0'",
3666
- rawTx: '80',
3667
- },
3668
- });
3425
+ const solSignMessageMethod = new SolSignMessage({
3426
+ id: 1,
3427
+ payload: {
3428
+ method: 'solSignMessage',
3429
+ path: "m/44'/501'/0'/0'",
3430
+ messageHex: '48656c6c6f',
3431
+ },
3432
+ });
3433
+ const solSignOffchainMessageMethod = new SolSignOffchainMessage({
3434
+ id: 2,
3435
+ payload: {
3436
+ method: 'solSignOffchainMessage',
3437
+ path: "m/44'/501'/0'/0'",
3438
+ messageHex: '48656c6c6f',
3439
+ },
3440
+ });
3441
+ const solSignTransactionMethod = new SolSignTransaction({
3442
+ id: 3,
3443
+ payload: {
3444
+ method: 'solSignTransaction',
3445
+ path: "m/44'/501'/0'/0'",
3446
+ rawTx: '00',
3447
+ },
3448
+ });
3449
+ const solSignVersionedTransactionMethod = new SolSignTransaction({
3450
+ id: 4,
3451
+ payload: {
3452
+ method: 'solSignTransaction',
3453
+ path: "m/44'/501'/0'/0'",
3454
+ rawTx: '80',
3455
+ },
3456
+ });
3669
3457
 
3670
- solSignTransactionMethod.init();
3671
- solSignVersionedTransactionMethod.init();
3458
+ solSignTransactionMethod.init();
3459
+ solSignVersionedTransactionMethod.init();
3672
3460
 
3673
- expect(
3674
- getMethodVersionRange(features, type => solSignMessageMethod.getVersionRange()[type])
3675
- ).toEqual({
3676
- min: '0.0.0',
3677
- });
3678
- expect(
3679
- getMethodVersionRange(
3680
- features,
3681
- type => solSignOffchainMessageMethod.getVersionRange()[type]
3682
- )
3683
- ).toEqual({
3684
- min: '0.0.0',
3685
- });
3686
- expect(
3687
- getMethodVersionRange(features, type => solSignTransactionMethod.getVersionRange()[type])
3688
- ).toEqual({
3689
- min: '0.0.0',
3690
- });
3691
- expect(
3692
- getMethodVersionRange(
3693
- features,
3694
- type => solSignVersionedTransactionMethod.getVersionRange()[type]
3695
- )
3696
- ).toEqual({
3697
- min: '0.0.0',
3698
- });
3699
- }
3700
- );
3461
+ expect(
3462
+ getMethodVersionRange(features, type => solSignMessageMethod.getVersionRange()[type])
3463
+ ).toEqual({
3464
+ min: '0.0.0',
3465
+ });
3466
+ expect(
3467
+ getMethodVersionRange(features, type => solSignOffchainMessageMethod.getVersionRange()[type])
3468
+ ).toEqual({
3469
+ min: '0.0.0',
3470
+ });
3471
+ expect(
3472
+ getMethodVersionRange(features, type => solSignTransactionMethod.getVersionRange()[type])
3473
+ ).toEqual({
3474
+ min: '0.0.0',
3475
+ });
3476
+ expect(
3477
+ getMethodVersionRange(
3478
+ features,
3479
+ type => solSignVersionedTransactionMethod.getVersionRange()[type]
3480
+ )
3481
+ ).toEqual({
3482
+ min: '0.0.0',
3483
+ });
3484
+ });
3701
3485
 
3702
3486
  test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
3703
3487
  const rawTx = '0x'.concat('ab'.repeat(5000));
@@ -3793,15 +3577,6 @@ describe('API compatibility handling', () => {
3793
3577
 
3794
3578
  describe('Protocol V2 firmware update targets', () => {
3795
3579
  const OKPP_HEADER_SIZE = 0x52a0;
3796
- let forceReloadDataSpy: jest.SpyInstance;
3797
-
3798
- beforeEach(() => {
3799
- forceReloadDataSpy = jest.spyOn(DataManager, 'forceReloadData').mockResolvedValue(undefined);
3800
- });
3801
-
3802
- afterEach(() => {
3803
- jest.restoreAllMocks();
3804
- });
3805
3580
 
3806
3581
  const buildOkppHeader = ({
3807
3582
  type = 'RESC',
@@ -3926,7 +3701,7 @@ describe('Protocol V2 firmware update targets', () => {
3926
3701
  specific: true,
3927
3702
  },
3928
3703
  },
3929
- { timeoutMs: 30000 }
3704
+ { timeoutMs: 5000 }
3930
3705
  );
3931
3706
  expect(typedCall).toHaveBeenCalledTimes(3);
3932
3707
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
@@ -3942,7 +3717,7 @@ describe('Protocol V2 firmware update targets', () => {
3942
3717
  });
3943
3718
  });
3944
3719
 
3945
- test('enters Protocol V2 bootloader before upload', async () => {
3720
+ test('delegates Protocol V2 mode transition to the phase executor', async () => {
3946
3721
  const method = new FirmwareUpdateV4({
3947
3722
  id: 1,
3948
3723
  payload: {
@@ -3977,7 +3752,7 @@ describe('Protocol V2 firmware update targets', () => {
3977
3752
 
3978
3753
  await method.run();
3979
3754
 
3980
- expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
3755
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
3981
3756
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3982
3757
  fwBinaryMap: [
3983
3758
  {
@@ -3988,72 +3763,9 @@ describe('Protocol V2 firmware update targets', () => {
3988
3763
  ],
3989
3764
  bootloaderBinary: null,
3990
3765
  });
3991
- expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
3992
3766
  });
3993
3767
 
3994
- test('enters Protocol V2 bootloader before reading and downloading remote resources', async () => {
3995
- const method = new FirmwareUpdateV4({
3996
- id: 1,
3997
- payload: {
3998
- method: 'firmwareUpdateV4',
3999
- platform: 'web',
4000
- targetsToUpdate: ['resource'],
4001
- },
4002
- });
4003
- method.init();
4004
-
4005
- (method as any).device = stubDevice({
4006
- originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
4007
- features: {
4008
- deviceType: 'pro2',
4009
- firmwareVersion: '1.0.0',
4010
- mode: 'normal',
4011
- bootloaderMode: false,
4012
- capabilities: [],
4013
- },
4014
- isBootloader: () => false,
4015
- isRomloader: () => false,
4016
- });
4017
- (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4018
- (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
4019
- bootloaderBinary: null,
4020
- fwBinaryMap: [],
4021
- installItems: [],
4022
- });
4023
-
4024
- const order: string[] = [];
4025
- (method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
4026
- order.push('enter-bootloader');
4027
- return Promise.resolve(true);
4028
- });
4029
- (method as any).prepareProtocolV2ResourceBundles = jest.fn().mockImplementation(() => {
4030
- order.push('prepare-resources');
4031
- return Promise.resolve([
4032
- {
4033
- name: 'images.okpkg',
4034
- binary: new Uint8Array([1]).buffer,
4035
- devicePath: 'vol0:/bundles/images/images.okpkg',
4036
- },
4037
- ]);
4038
- });
4039
- (method as any).executeProtocolV2Update = jest.fn().mockImplementation(() => {
4040
- order.push('execute-update');
4041
- return Promise.resolve();
4042
- });
4043
- (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
4044
- (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
4045
- bootloaderVersion: '1.0.0',
4046
- bleVersion: '1.0.0',
4047
- firmwareVersion: '1.0.0',
4048
- });
4049
- method.postTipMessage = jest.fn();
4050
-
4051
- await method.run();
4052
-
4053
- expect(order).toEqual(['enter-bootloader', 'prepare-resources', 'execute-update']);
4054
- });
4055
-
4056
- test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
3768
+ test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
4057
3769
  const method = new FirmwareUpdateV4({
4058
3770
  id: 1,
4059
3771
  payload: {
@@ -4064,10 +3776,6 @@ describe('Protocol V2 firmware update targets', () => {
4064
3776
  originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
4065
3777
  features: { bootloader_mode: false, capabilities: [] },
4066
3778
  isBootloader: () => false,
4067
- probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
4068
- mode: 'bootloader',
4069
- bootloaderMode: true,
4070
- }),
4071
3779
  });
4072
3780
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
4073
3781
  message: 'Device rebooted successfully',
@@ -4095,7 +3803,6 @@ describe('Protocol V2 firmware update targets', () => {
4095
3803
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4096
3804
  (method as any).device.getCommands = () => ({ typedCall });
4097
3805
  (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
4098
- (method as any).protocolV2ExpectedPath = 'usb-path';
4099
3806
  method.postTipMessage = jest.fn();
4100
3807
 
4101
3808
  await (method as any).enterProtocolV2BootloaderMode();
@@ -4107,66 +3814,6 @@ describe('Protocol V2 firmware update targets', () => {
4107
3814
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4108
3815
  });
4109
3816
 
4110
- test('announces bootloader success only after Protocol V2 bootloader mode is confirmed', async () => {
4111
- const method = new FirmwareUpdateV4({
4112
- id: 1,
4113
- payload: {
4114
- method: 'firmwareUpdateV4',
4115
- },
4116
- });
4117
- (method as any).device = stubDevice({
4118
- features: { bootloader_mode: false, capabilities: [] },
4119
- isBootloader: () => false,
4120
- isRomloader: () => false,
4121
- });
4122
- (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
4123
- message: 'Device rebooted successfully',
4124
- });
4125
-
4126
- const tipMessages: string[] = [];
4127
- method.postTipMessage = jest.fn((message: string) => {
4128
- tipMessages.push(message);
4129
- });
4130
- let successWasAnnouncedBeforeConfirmation = false;
4131
- (method as any).waitForProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
4132
- successWasAnnouncedBeforeConfirmation = tipMessages.includes('GoToBootloaderSuccess');
4133
- return Promise.resolve({ mode: 'bootloader' });
4134
- });
4135
-
4136
- await (method as any).enterProtocolV2BootloaderMode();
4137
-
4138
- expect(successWasAnnouncedBeforeConfirmation).toBe(false);
4139
- expect(tipMessages).toEqual(['AutoRebootToBootloader', 'GoToBootloaderSuccess']);
4140
- });
4141
-
4142
- test('keeps Protocol V2 bootloader active before firmware transfer', async () => {
4143
- const method = new FirmwareUpdateV4({
4144
- id: 1,
4145
- payload: {
4146
- method: 'firmwareUpdateV4',
4147
- },
4148
- });
4149
- (method as any).device = stubDevice({
4150
- originalDescriptor: { id: 'usb-id', path: 'bootloader-path', protocolType: 'V2' },
4151
- features: {
4152
- deviceType: 'pro2',
4153
- mode: 'bootloader',
4154
- bootloaderMode: true,
4155
- capabilities: [],
4156
- },
4157
- isBootloader: () => true,
4158
- isRomloader: () => false,
4159
- });
4160
- (method as any).protocolV2Reboot = jest.fn();
4161
- method.postTipMessage = jest.fn();
4162
-
4163
- await expect((method as any).enterProtocolV2BootloaderMode()).resolves.toBe(false);
4164
-
4165
- expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
4166
- expect(method.postTipMessage).not.toHaveBeenCalledWith('AutoRebootToBootloader');
4167
- expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
4168
- });
4169
-
4170
3817
  test('keeps Protocol V2 romloader active before firmware transfer', async () => {
4171
3818
  const method = new FirmwareUpdateV4({
4172
3819
  id: 1,
@@ -4219,9 +3866,6 @@ describe('Protocol V2 firmware update targets', () => {
4219
3866
  },
4220
3867
  });
4221
3868
  (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
4222
- const prepareResourceBundles = jest
4223
- .spyOn(method as any, 'prepareProtocolV2ResourceBundles')
4224
- .mockResolvedValue(undefined);
4225
3869
  (method as any).protocolV2Reboot = jest.fn();
4226
3870
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
4227
3871
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
@@ -4234,7 +3878,6 @@ describe('Protocol V2 firmware update targets', () => {
4234
3878
 
4235
3879
  await method.run();
4236
3880
 
4237
- expect(prepareResourceBundles).not.toHaveBeenCalled();
4238
3881
  expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
4239
3882
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
4240
3883
  bootloaderBinary: null,
@@ -4254,10 +3897,6 @@ describe('Protocol V2 firmware update targets', () => {
4254
3897
  originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
4255
3898
  features: { bootloader_mode: false, capabilities: [] },
4256
3899
  isBootloader: () => false,
4257
- probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
4258
- mode: 'bootloader',
4259
- bootloaderMode: true,
4260
- }),
4261
3900
  });
4262
3901
  const typedCall = jest.fn().mockImplementation((requestType: string) => {
4263
3902
  if (requestType === 'DeviceInfoGet') {
@@ -4285,13 +3924,15 @@ describe('Protocol V2 firmware update targets', () => {
4285
3924
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4286
3925
  (method as any).device.getCommands = () => ({ typedCall });
4287
3926
  (method as any).protocolV2ExpectedSerialNumber = 'PR9999999999';
4288
- (method as any).protocolV2ExpectedPath = 'app-path';
4289
3927
 
4290
3928
  await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
4291
3929
 
4292
3930
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
4293
- expect(typedCall).toHaveBeenCalledTimes(1);
4294
- expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
3931
+ expect(typedCall).toHaveBeenCalledTimes(2);
3932
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
3933
+ 'DeviceInfoGet',
3934
+ 'ProtocolInfoRequest',
3935
+ ]);
4295
3936
  });
4296
3937
 
4297
3938
  test('does not run generic initialize during Protocol V2 USB firmware reconnect', async () => {
@@ -4319,16 +3960,12 @@ describe('Protocol V2 firmware update targets', () => {
4319
3960
  getCommands: () => commands,
4320
3961
  mainId: 'usb-path',
4321
3962
  });
4322
- const cachedDevice = {
4323
- getConnectId: () => 'usb-path',
4324
- getCurrentSerialNo: () => '',
4325
- };
3963
+ const cachedDevice = { getConnectId: () => 'usb-path' };
4326
3964
  const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
4327
3965
  devices: {},
4328
3966
  deviceList: [cachedDevice],
4329
3967
  } as any);
4330
3968
  (method as any).device = device;
4331
- (method as any).protocolV2ExpectedPath = 'usb-path';
4332
3969
 
4333
3970
  try {
4334
3971
  await (method as any).reconnectProtocolV2Device();
@@ -4340,184 +3977,6 @@ describe('Protocol V2 firmware update targets', () => {
4340
3977
  expect(initialize).not.toHaveBeenCalled();
4341
3978
  });
4342
3979
 
4343
- test('selects the matching physical device from multiple Protocol V2 USB reconnect candidates', async () => {
4344
- const method = new FirmwareUpdateV4({
4345
- id: 1,
4346
- payload: {
4347
- method: 'firmwareUpdateV4',
4348
- },
4349
- });
4350
- const commands = { disposed: true, mainId: '' };
4351
- const updateFromCache = jest.fn();
4352
- const device = stubDevice({
4353
- originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
4354
- deviceConnector: {
4355
- enumerate: jest.fn().mockResolvedValue({
4356
- descriptors: [
4357
- { id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
4358
- { id: 'usb-b', path: 'usb-b', protocolType: 'V2' },
4359
- ],
4360
- }),
4361
- },
4362
- updateFromCache,
4363
- hasDeviceAcquire: jest.fn(() => false),
4364
- acquire: jest.fn().mockResolvedValue(undefined),
4365
- commands,
4366
- getCommands: () => commands,
4367
- mainId: 'old-usb-path',
4368
- });
4369
- const otherDevice = {
4370
- getConnectId: () => 'usb-a',
4371
- getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
4372
- };
4373
- const expectedDevice = {
4374
- getConnectId: () => 'usb-b',
4375
- getCurrentSerialNo: () => 'PRO2-PHYSICAL-B',
4376
- };
4377
- const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
4378
- devices: {},
4379
- deviceList: [otherDevice, expectedDevice],
4380
- } as any);
4381
- (method as any).device = device;
4382
- (method as any).protocolV2ExpectedPath = 'usb-path';
4383
- (method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-B';
4384
-
4385
- try {
4386
- await (method as any).reconnectProtocolV2Device();
4387
- } finally {
4388
- getDevices.mockRestore();
4389
- }
4390
-
4391
- expect(updateFromCache).toHaveBeenCalledWith(expectedDevice);
4392
- });
4393
-
4394
- test('rejects ambiguous Protocol V2 USB reconnect candidates without an identity match', async () => {
4395
- const method = new FirmwareUpdateV4({
4396
- id: 1,
4397
- payload: {
4398
- method: 'firmwareUpdateV4',
4399
- },
4400
- });
4401
- const updateFromCache = jest.fn();
4402
- const device = stubDevice({
4403
- originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
4404
- deviceConnector: {
4405
- enumerate: jest.fn().mockResolvedValue({
4406
- descriptors: [
4407
- { id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
4408
- { id: 'usb-b', path: 'usb-b', protocolType: 'V2' },
4409
- ],
4410
- }),
4411
- },
4412
- updateFromCache,
4413
- });
4414
- const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
4415
- devices: {},
4416
- deviceList: [
4417
- {
4418
- getConnectId: () => 'usb-a',
4419
- getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
4420
- },
4421
- {
4422
- getConnectId: () => 'usb-b',
4423
- getCurrentSerialNo: () => 'PRO2-PHYSICAL-B',
4424
- },
4425
- ],
4426
- } as any);
4427
- (method as any).device = device;
4428
- (method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-C';
4429
-
4430
- try {
4431
- await expect((method as any).reconnectProtocolV2Device()).rejects.toBeDefined();
4432
- } finally {
4433
- getDevices.mockRestore();
4434
- }
4435
-
4436
- expect(updateFromCache).not.toHaveBeenCalled();
4437
- });
4438
-
4439
- test('allows a Protocol V2 USB reconnect candidate without a serial only when its path matches', async () => {
4440
- const method = new FirmwareUpdateV4({
4441
- id: 1,
4442
- payload: {
4443
- method: 'firmwareUpdateV4',
4444
- },
4445
- });
4446
- const commands = { disposed: true, mainId: '' };
4447
- const updateFromCache = jest.fn();
4448
- const device = stubDevice({
4449
- originalDescriptor: { id: 'usb-a', path: 'usb-a', protocolType: 'V2' },
4450
- deviceConnector: {
4451
- enumerate: jest.fn().mockResolvedValue({
4452
- descriptors: [{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' }],
4453
- }),
4454
- },
4455
- updateFromCache,
4456
- hasDeviceAcquire: jest.fn(() => false),
4457
- acquire: jest.fn().mockResolvedValue(undefined),
4458
- commands,
4459
- getCommands: () => commands,
4460
- mainId: 'old-usb-path',
4461
- });
4462
- const candidate = {
4463
- getConnectId: () => 'usb-a',
4464
- getCurrentSerialNo: () => '',
4465
- };
4466
- const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
4467
- devices: {},
4468
- deviceList: [candidate],
4469
- } as any);
4470
- (method as any).device = device;
4471
- (method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-A';
4472
- (method as any).protocolV2ExpectedPath = 'usb-a';
4473
-
4474
- try {
4475
- await (method as any).reconnectProtocolV2Device();
4476
- } finally {
4477
- getDevices.mockRestore();
4478
- }
4479
-
4480
- expect(updateFromCache).toHaveBeenCalledWith(candidate);
4481
- });
4482
-
4483
- test('rejects a single Protocol V2 USB reconnect candidate with a different physical serial', async () => {
4484
- const method = new FirmwareUpdateV4({
4485
- id: 1,
4486
- payload: {
4487
- method: 'firmwareUpdateV4',
4488
- },
4489
- });
4490
- const updateFromCache = jest.fn();
4491
- const device = stubDevice({
4492
- originalDescriptor: { id: 'old-usb-id', path: 'old-usb-path', protocolType: 'V2' },
4493
- deviceConnector: {
4494
- enumerate: jest.fn().mockResolvedValue({
4495
- descriptors: [{ id: 'usb-a', path: 'usb-a', protocolType: 'V2' }],
4496
- }),
4497
- },
4498
- updateFromCache,
4499
- });
4500
- const getDevices = jest.spyOn(DevicePool, 'getDevices').mockResolvedValue({
4501
- devices: {},
4502
- deviceList: [
4503
- {
4504
- getConnectId: () => 'usb-a',
4505
- getCurrentSerialNo: () => 'PRO2-PHYSICAL-A',
4506
- },
4507
- ],
4508
- } as any);
4509
- (method as any).device = device;
4510
- (method as any).protocolV2ExpectedSerialNumber = 'PRO2-PHYSICAL-B';
4511
-
4512
- try {
4513
- await expect((method as any).reconnectProtocolV2Device()).rejects.toBeDefined();
4514
- } finally {
4515
- getDevices.mockRestore();
4516
- }
4517
-
4518
- expect(updateFromCache).not.toHaveBeenCalled();
4519
- });
4520
-
4521
3980
  test('reuses an acquired Protocol V2 USB session while polling reconnect readiness', async () => {
4522
3981
  const method = new FirmwareUpdateV4({
4523
3982
  id: 1,
@@ -4559,7 +4018,6 @@ describe('Protocol V2 firmware update targets', () => {
4559
4018
  deviceList: [{ getConnectId: () => 'usb-path' }],
4560
4019
  } as any);
4561
4020
  (method as any).device = device;
4562
- (method as any).protocolV2ExpectedPath = 'usb-path';
4563
4021
 
4564
4022
  try {
4565
4023
  await (method as any).reconnectProtocolV2Device();
@@ -4590,7 +4048,6 @@ describe('Protocol V2 firmware update targets', () => {
4590
4048
  (method as any).protocolV2ExpectedSerialNumber = 'expected-serial';
4591
4049
  (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4592
4050
  (method as any).device = stubDevice({
4593
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
4594
4051
  getCommands: () => ({ typedCall }),
4595
4052
  probeProtocolV2RuntimeState: jest.fn().mockResolvedValue({
4596
4053
  deviceId: 'changed-device',
@@ -4614,9 +4071,7 @@ describe('Protocol V2 firmware update targets', () => {
4614
4071
  });
4615
4072
  method.postTipMessage = jest.fn();
4616
4073
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4617
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
4618
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
4619
- (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(false);
4074
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
4620
4075
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
4621
4076
  message: 'Device rebooted successfully',
4622
4077
  });
@@ -4625,29 +4080,9 @@ describe('Protocol V2 firmware update targets', () => {
4625
4080
 
4626
4081
  expect(method.postTipMessage).not.toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
4627
4082
  expect((method as any).reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4628
- expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4629
4083
  expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal);
4630
4084
  });
4631
4085
 
4632
- test('skips a second Normal reboot after Pro2 already returned to App mode', async () => {
4633
- const method = new FirmwareUpdateV4({
4634
- id: 1,
4635
- payload: {
4636
- method: 'firmwareUpdateV4',
4637
- },
4638
- });
4639
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
4640
- (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4641
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
4642
- (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
4643
- (method as any).protocolV2Reboot = jest.fn();
4644
-
4645
- await (method as any).exitProtocolV2BootloaderToNormal();
4646
-
4647
- expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4648
- expect((method as any).protocolV2Reboot).not.toHaveBeenCalled();
4649
- });
4650
-
4651
4086
  test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
4652
4087
  const method = new FirmwareUpdateV4({
4653
4088
  id: 1,
@@ -4690,48 +4125,27 @@ describe('Protocol V2 firmware update targets', () => {
4690
4125
  });
4691
4126
  });
4692
4127
 
4693
- test('treats Android BLE transport release during Protocol V2 reboot as expected', async () => {
4128
+ test('requires an explicit Protocol V2 update ACK before entering install state', async () => {
4694
4129
  const method = new FirmwareUpdateV4({
4695
4130
  id: 1,
4696
4131
  payload: {
4697
4132
  method: 'firmwareUpdateV4',
4698
4133
  },
4699
4134
  });
4700
- const typedCall = jest.fn().mockRejectedValue(new Error('React Native BLE transport released'));
4135
+ const typedCall = jest
4136
+ .fn()
4137
+ .mockRejectedValue(
4138
+ Object.assign(
4139
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
4140
+ { name: 'NotFoundError' }
4141
+ )
4142
+ );
4701
4143
 
4702
4144
  (method as any).device = stubDevice({
4703
4145
  getCommands: () => ({ typedCall }),
4704
4146
  });
4705
-
4706
- await expect((method as any).protocolV2Reboot(DeviceRebootType.Bootloader)).resolves.toEqual({
4707
- message: 'Device rebooted successfully',
4708
- });
4709
- expect((method as any).device.markProtocolV2Reboot).toHaveBeenCalledWith(
4710
- DeviceRebootType.Bootloader
4711
- );
4712
- });
4713
-
4714
- test('requires an explicit Protocol V2 update ACK before entering install state', async () => {
4715
- const method = new FirmwareUpdateV4({
4716
- id: 1,
4717
- payload: {
4718
- method: 'firmwareUpdateV4',
4719
- },
4720
- });
4721
- const typedCall = jest
4722
- .fn()
4723
- .mockRejectedValue(
4724
- new DOMException(
4725
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
4726
- 'NotFoundError'
4727
- )
4728
- );
4729
-
4730
- (method as any).device = stubDevice({
4731
- getCommands: () => ({ typedCall }),
4732
- });
4733
- method.postTipMessage = jest.fn();
4734
- method.postProgressMessage = jest.fn();
4147
+ method.postTipMessage = jest.fn();
4148
+ method.postProgressMessage = jest.fn();
4735
4149
 
4736
4150
  await expect(
4737
4151
  (method as any).protocolV2StartFirmwareUpdate({
@@ -4801,7 +4215,7 @@ describe('Protocol V2 firmware update targets', () => {
4801
4215
  { target_id: 4, path: 'vol0:/application_p1.bin' },
4802
4216
  ]);
4803
4217
 
4804
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
4218
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
4805
4219
  expect(typedCall.mock.calls.map(call => call[0])).toEqual([
4806
4220
  'DeviceFirmwareUpdateStatusGet',
4807
4221
  'DeviceFirmwareUpdateStatusGet',
@@ -4839,46 +4253,6 @@ describe('Protocol V2 firmware update targets', () => {
4839
4253
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4840
4254
  expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceFirmwareUpdateStatusGet']);
4841
4255
  expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4842
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
4843
- });
4844
-
4845
- test('accepts an empty firmware status dump only after confirming normal mode', async () => {
4846
- const method = new FirmwareUpdateV4({
4847
- id: 1,
4848
- payload: {
4849
- method: 'firmwareUpdateV4',
4850
- },
4851
- });
4852
- const typedCall = jest.fn().mockResolvedValue({
4853
- type: 'DeviceFirmwareUpdateStatus',
4854
- message: { records: [] },
4855
- });
4856
- const deviceInfo = { hw: { serial_no: 'PRO2-PHYSICAL-1' } };
4857
- let now = 0;
4858
- jest.spyOn(Date, 'now').mockImplementation(() => now);
4859
- jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
4860
- now += 31 * 1000;
4861
- callback();
4862
- return 0 as any;
4863
- }) as typeof setTimeout);
4864
-
4865
- (method as any).device = stubDevice({
4866
- getCommands: () => ({ typedCall }),
4867
- });
4868
- (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4869
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(deviceInfo);
4870
- (method as any).probeProtocolV2NormalMode = jest.fn().mockResolvedValue(true);
4871
- method.postProgressMessage = jest.fn();
4872
-
4873
- await expect(
4874
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
4875
- { target_id: 4, path: 'vol0:/application_p1.bin' },
4876
- ])
4877
- ).resolves.toBeUndefined();
4878
-
4879
- expect(typedCall).toHaveBeenCalledTimes(1);
4880
- expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4881
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
4882
4256
  });
4883
4257
 
4884
4258
  test.each([
@@ -4944,65 +4318,54 @@ describe('Protocol V2 firmware update targets', () => {
4944
4318
  expect((method as any).probeProtocolV2NormalMode).toHaveBeenCalledWith(deviceInfo);
4945
4319
  });
4946
4320
 
4947
- test('polls target status after update ACK and finishes when all targets complete', async () => {
4321
+ test('rejects a finished target whose payload version conflicts with the plan', () => {
4948
4322
  const method = new FirmwareUpdateV4({
4949
4323
  id: 1,
4950
4324
  payload: {
4951
4325
  method: 'firmwareUpdateV4',
4326
+ platform: 'desktop',
4327
+ expectedTargetVersions: {
4328
+ app_v1: '2.0.0',
4329
+ },
4952
4330
  },
4953
4331
  });
4954
- const typedCall = jest.fn().mockResolvedValue({
4955
- type: 'DeviceFirmwareUpdateStatus',
4956
- message: {
4957
- records: [
4958
- { target_id: 3, status: 2 },
4959
- { target_id: 4, status: 2 },
4960
- ],
4961
- },
4962
- });
4963
- const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4964
-
4965
- (method as any).device = stubDevice({
4966
- getCommands: () => ({ typedCall }),
4967
- });
4968
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4969
- (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
4332
+ method.init();
4970
4333
  method.postProgressMessage = jest.fn();
4971
4334
 
4972
- await expect(
4973
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
4974
- { target_id: 3, path: 'vol1:bootloader.bin' },
4975
- { target_id: 4, path: 'vol1:application_p1.bin' },
4976
- ])
4977
- ).resolves.toBeUndefined();
4978
-
4979
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4980
- expect(typedCall).toHaveBeenCalledWith(
4981
- 'DeviceFirmwareUpdateStatusGet',
4982
- 'DeviceFirmwareUpdateStatus',
4983
- expect.anything(),
4984
- expect.anything()
4985
- );
4986
- expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
4335
+ expect(() =>
4336
+ (method as any).assertProtocolV2TargetStatus(
4337
+ [{ target_id: 4, status: 2, payload_version: 0x010000 }],
4338
+ new Set([4])
4339
+ )
4340
+ ).toThrow('expected 131072');
4987
4341
  });
4988
4342
 
4989
- test('fails clearly when a requested target stays absent from the full status dump', async () => {
4343
+ test('fails install polling immediately with the original firmware verification error', async () => {
4990
4344
  const method = new FirmwareUpdateV4({
4991
4345
  id: 1,
4992
4346
  payload: {
4993
4347
  method: 'firmwareUpdateV4',
4348
+ platform: 'desktop',
4349
+ expectedTargetVersions: {
4350
+ app_v1: '2.0.0',
4351
+ },
4994
4352
  },
4995
4353
  });
4354
+ method.init();
4996
4355
  const typedCall = jest.fn().mockResolvedValue({
4997
4356
  type: 'DeviceFirmwareUpdateStatus',
4998
4357
  message: {
4999
- records: [{ target_id: 4, status: 2 }],
4358
+ records: [{ target_id: 4, status: 2, payload_version: 0x010000 }],
5000
4359
  },
5001
4360
  });
5002
- let now = 0;
5003
- jest.spyOn(Date, 'now').mockImplementation(() => now);
5004
- jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
5005
- now += 31 * 1000;
4361
+ const dateNowSpy = jest
4362
+ .spyOn(Date, 'now')
4363
+ .mockReturnValueOnce(0)
4364
+ .mockReturnValueOnce(0)
4365
+ .mockReturnValue(300_001);
4366
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
4367
+ callback: () => void
4368
+ ) => {
5006
4369
  callback();
5007
4370
  return 0 as any;
5008
4371
  }) as typeof setTimeout);
@@ -5012,52 +4375,45 @@ describe('Protocol V2 firmware update targets', () => {
5012
4375
  });
5013
4376
  (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5014
4377
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5015
- method.postProgressMessage = jest.fn();
5016
4378
 
5017
- await expect(
5018
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
5019
- { target_id: 3, path: 'vol0:/bootloader.bin' },
5020
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5021
- ])
5022
- ).rejects.toMatchObject({
5023
- errorCode: HardwareErrorCode.FirmwareError,
5024
- message: expect.stringContaining('targetIds=3'),
5025
- });
5026
- expect(typedCall).toHaveBeenCalledTimes(2);
4379
+ try {
4380
+ await expect(
4381
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
4382
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
4383
+ ])
4384
+ ).rejects.toMatchObject({
4385
+ errorCode: HardwareErrorCode.FirmwareVerificationFailed,
4386
+ message: expect.stringContaining('reached payload version 65536, expected 131072'),
4387
+ });
4388
+ } finally {
4389
+ dateNowSpy.mockRestore();
4390
+ setTimeoutSpy.mockRestore();
4391
+ }
4392
+
4393
+ expect(typedCall).toHaveBeenCalledTimes(1);
5027
4394
  });
5028
4395
 
5029
- test('resets the missing-record grace period after a Pro2 reboot interruption', async () => {
4396
+ test('fails install polling immediately with the original status conflict error', async () => {
5030
4397
  const method = new FirmwareUpdateV4({
5031
4398
  id: 1,
5032
4399
  payload: {
5033
4400
  method: 'firmwareUpdateV4',
5034
4401
  },
5035
4402
  });
5036
- const typedCall = jest
5037
- .fn()
5038
- .mockResolvedValueOnce({
5039
- type: 'DeviceFirmwareUpdateStatus',
5040
- message: { records: [{ target_id: 4, status: 2 }] },
5041
- })
5042
- .mockRejectedValueOnce(new Error('Pro2 is rebooting'))
5043
- .mockResolvedValueOnce({
5044
- type: 'DeviceFirmwareUpdateStatus',
5045
- message: { records: [{ target_id: 4, status: 2 }] },
5046
- })
5047
- .mockResolvedValueOnce({
5048
- type: 'DeviceFirmwareUpdateStatus',
5049
- message: {
5050
- records: [
5051
- { target_id: 3, status: 2 },
5052
- { target_id: 4, status: 2 },
5053
- ],
5054
- },
5055
- });
5056
- const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5057
- let now = 0;
5058
- jest.spyOn(Date, 'now').mockImplementation(() => now);
5059
- jest.spyOn(global, 'setTimeout').mockImplementation(((callback: () => void) => {
5060
- now += 31 * 1000;
4403
+ const typedCall = jest.fn().mockResolvedValue({
4404
+ type: 'DeviceFirmwareUpdateStatus',
4405
+ message: {
4406
+ records: [{ target_id: 4, status: 2, path: 'vol0:/unexpected.bin' }],
4407
+ },
4408
+ });
4409
+ const dateNowSpy = jest
4410
+ .spyOn(Date, 'now')
4411
+ .mockReturnValueOnce(0)
4412
+ .mockReturnValueOnce(0)
4413
+ .mockReturnValue(300_001);
4414
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(((
4415
+ callback: () => void
4416
+ ) => {
5061
4417
  callback();
5062
4418
  return 0 as any;
5063
4419
  }) as typeof setTimeout);
@@ -5065,70 +4421,118 @@ describe('Protocol V2 firmware update targets', () => {
5065
4421
  (method as any).device = stubDevice({
5066
4422
  getCommands: () => ({ typedCall }),
5067
4423
  });
5068
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4424
+ (method as any).reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
5069
4425
  (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
5070
- method.postProgressMessage = jest.fn();
5071
4426
 
5072
- await expect(
5073
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
5074
- { target_id: 3, path: 'vol0:/bootloader.bin' },
5075
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5076
- ])
5077
- ).resolves.toBeUndefined();
5078
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(2);
5079
- expect(typedCall).toHaveBeenCalledTimes(4);
4427
+ try {
4428
+ await expect(
4429
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
4430
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
4431
+ ])
4432
+ ).rejects.toMatchObject({
4433
+ errorCode: HardwareErrorCode.RuntimeError,
4434
+ message: 'Protocol V2 install status conflicts with target 4',
4435
+ params: {
4436
+ firmwareUpdateCode: 'FirmwareInstallStatusConflict',
4437
+ },
4438
+ });
4439
+ } finally {
4440
+ dateNowSpy.mockRestore();
4441
+ setTimeoutSpy.mockRestore();
4442
+ }
4443
+
4444
+ expect(typedCall).toHaveBeenCalledTimes(1);
5080
4445
  });
5081
4446
 
5082
- test('uses an eight-minute Pro2 install status window', async () => {
4447
+ test('does not fail a completed multi-app install when final app versions are unobservable', () => {
5083
4448
  const method = new FirmwareUpdateV4({
5084
4449
  id: 1,
5085
4450
  payload: {
5086
4451
  method: 'firmwareUpdateV4',
4452
+ platform: 'desktop',
4453
+ targetsToUpdate: ['app_v1', 'app_v2'],
4454
+ expectedTargetVersions: {
4455
+ app_v1: '1.0.0',
4456
+ app_v2: '2.0.0',
4457
+ },
5087
4458
  },
5088
4459
  });
5089
- jest
5090
- .spyOn(Date, 'now')
5091
- .mockReturnValueOnce(0)
5092
- .mockReturnValue(8 * 60 * 1000);
4460
+ method.init();
4461
+ (method as any).protocolV2LatestFinalFeatures = {
4462
+ major_version: 3,
4463
+ minor_version: 0,
4464
+ patch_version: 0,
4465
+ };
4466
+ (method as any).protocolV2FinalStatusVerified = true;
5093
4467
 
5094
- await expect(
5095
- (method as any).waitForProtocolV2FirmwareUpdateComplete([
5096
- { target_id: 4, path: 'vol0:/application_p1.bin' },
5097
- ])
5098
- ).rejects.toThrow('within 480s');
4468
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
5099
4469
  });
5100
4470
 
5101
- test('uses a 90-second Pro2 bootloader reconnect window', async () => {
4471
+ test('rejects unobservable final versions after the status endpoint fallback', () => {
5102
4472
  const method = new FirmwareUpdateV4({
5103
4473
  id: 1,
5104
4474
  payload: {
5105
4475
  method: 'firmwareUpdateV4',
4476
+ platform: 'desktop',
4477
+ targetsToUpdate: ['app_v1', 'app_v2'],
4478
+ expectedTargetVersions: {
4479
+ app_v1: '1.0.0',
4480
+ app_v2: '2.0.0',
4481
+ },
5106
4482
  },
5107
4483
  });
5108
- jest
5109
- .spyOn(Date, 'now')
5110
- .mockReturnValueOnce(0)
5111
- .mockReturnValue(90 * 1000);
4484
+ method.init();
4485
+ (method as any).protocolV2LatestFinalFeatures = {
4486
+ major_version: 3,
4487
+ minor_version: 0,
4488
+ patch_version: 0,
4489
+ };
5112
4490
 
5113
- await expect((method as any).waitForProtocolV2BootloaderMode()).rejects.toThrow('within 90s');
4491
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).toThrow(
4492
+ 'has no observable final version after status fallback'
4493
+ );
5114
4494
  });
5115
4495
 
5116
- test('uses a three-minute Pro2 final reconnect window', async () => {
4496
+ test('polls target status after update ACK and finishes when all targets complete', async () => {
5117
4497
  const method = new FirmwareUpdateV4({
5118
4498
  id: 1,
5119
4499
  payload: {
5120
4500
  method: 'firmwareUpdateV4',
5121
4501
  },
5122
4502
  });
5123
- const waitForProtocolV2ReconnectAndFeatures = jest
5124
- .fn()
5125
- .mockRejectedValue(new Error('stop after capturing timeout'));
5126
- (method as any).waitForProtocolV2ReconnectAndFeatures = waitForProtocolV2ReconnectAndFeatures;
4503
+ const typedCall = jest.fn().mockResolvedValue({
4504
+ type: 'DeviceFirmwareUpdateStatus',
4505
+ message: {
4506
+ records: [
4507
+ { target_id: 3, status: 2 },
4508
+ { target_id: 4, status: 2 },
4509
+ ],
4510
+ },
4511
+ });
4512
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
4513
+
4514
+ (method as any).device = stubDevice({
4515
+ getCommands: () => ({ typedCall }),
4516
+ });
4517
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
4518
+ (method as any).verifyProtocolV2ReconnectIdentity = jest.fn().mockResolvedValue(undefined);
4519
+ method.postProgressMessage = jest.fn();
5127
4520
 
5128
- await expect((method as any).waitForProtocolV2FinalFeatures()).rejects.toThrow(
5129
- 'stop after capturing timeout'
4521
+ await expect(
4522
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
4523
+ { target_id: 3, path: 'vol1:bootloader.bin' },
4524
+ { target_id: 4, path: 'vol1:application_p1.bin' },
4525
+ ])
4526
+ ).resolves.toBeUndefined();
4527
+
4528
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
4529
+ expect(typedCall).toHaveBeenCalledWith(
4530
+ 'DeviceFirmwareUpdateStatusGet',
4531
+ 'DeviceFirmwareUpdateStatus',
4532
+ expect.anything(),
4533
+ expect.anything()
5130
4534
  );
5131
- expect(waitForProtocolV2ReconnectAndFeatures).toHaveBeenCalledWith(3 * 60 * 1000);
4535
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
5132
4536
  });
5133
4537
 
5134
4538
  test('uses SDK decoded enum names for Protocol V2 install polling', () => {
@@ -5165,7 +4569,7 @@ describe('Protocol V2 firmware update targets', () => {
5165
4569
  ).toBe(true);
5166
4570
 
5167
4571
  (method.postProgressMessage as jest.Mock).mockClear();
5168
- expect(
4572
+ expect(() =>
5169
4573
  (method as any).assertProtocolV2TargetStatus(
5170
4574
  [
5171
4575
  { target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
@@ -5173,8 +4577,8 @@ describe('Protocol V2 firmware update targets', () => {
5173
4577
  ],
5174
4578
  new Set([4, 10])
5175
4579
  )
5176
- ).toBe(false);
5177
- expect(method.postProgressMessage).toHaveBeenCalledWith(50, 'installingFirmware');
4580
+ ).toThrow('Protocol V2 install status conflicts with target 4');
4581
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
5178
4582
  expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
5179
4583
 
5180
4584
  expect(
@@ -5263,7 +4667,7 @@ describe('Protocol V2 firmware update targets', () => {
5263
4667
  path: true,
5264
4668
  },
5265
4669
  },
5266
- { timeoutMs: 15000 }
4670
+ { timeoutMs: 5000 }
5267
4671
  );
5268
4672
  });
5269
4673
 
@@ -5278,10 +4682,14 @@ describe('Protocol V2 firmware update targets', () => {
5278
4682
  const writtenPaths: string[] = [];
5279
4683
  method.postTipMessage = jest.fn();
5280
4684
  method.postProgressMessage = jest.fn();
5281
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
4685
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockImplementation(params => {
5282
4686
  writtenPaths.push(params.filePath);
5283
- return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
4687
+ return Number(params.processedSize ?? 0) + Number(params.source.size);
5284
4688
  });
4689
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
4690
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
4691
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({});
4692
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5285
4693
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5286
4694
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5287
4695
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5321,10 +4729,12 @@ describe('Protocol V2 firmware update targets', () => {
5321
4729
  'vol0:/coprocessor.bin',
5322
4730
  1
5323
4731
  );
5324
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1);
5325
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
4732
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(2);
4733
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
4734
+ targets: [{ target_id: 3, path: 'vol0:/bootloader.bin' }],
4735
+ });
4736
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
5326
4737
  targets: [
5327
- { target_id: 3, path: 'vol0:/bootloader.bin' },
5328
4738
  { target_id: 6, path: 'vol0:/coprocessor.bin' },
5329
4739
  { target_id: 7, path: 'vol0:/se01.bin' },
5330
4740
  { target_id: 4, path: 'vol0:/application_p1.bin' },
@@ -5334,7 +4744,7 @@ describe('Protocol V2 firmware update targets', () => {
5334
4744
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
5335
4745
  });
5336
4746
 
5337
- test('announces one transfer when syncing resource bundles and staging firmware', async () => {
4747
+ test('announces separate resource and component transfer phases', async () => {
5338
4748
  const method = new FirmwareUpdateV4({
5339
4749
  id: 1,
5340
4750
  payload: {
@@ -5344,11 +4754,14 @@ describe('Protocol V2 firmware update targets', () => {
5344
4754
 
5345
4755
  method.postTipMessage = jest.fn();
5346
4756
  method.postProgressMessage = jest.fn();
5347
- (method as any).protocolV2CommonUpdateProcess = jest
4757
+ (method as any).protocolV2SourceUpdateProcess = jest
5348
4758
  .fn()
5349
4759
  .mockImplementation(params =>
5350
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
4760
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
5351
4761
  );
4762
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
4763
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
4764
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5352
4765
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5353
4766
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5354
4767
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5368,21 +4781,22 @@ describe('Protocol V2 firmware update targets', () => {
5368
4781
  {
5369
4782
  fileName: 'application_p1.bin',
5370
4783
  binary: new Uint8Array([3]).buffer,
5371
- targetId: 3,
4784
+ targetId: 4,
5372
4785
  },
5373
4786
  ],
5374
4787
  });
5375
4788
 
5376
- expect(method.postTipMessage).toHaveBeenCalledTimes(2);
4789
+ expect(method.postTipMessage).toHaveBeenCalledTimes(3);
5377
4790
  expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData');
5378
- expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice');
5379
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
4791
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'StartTransferData');
4792
+ expect(method.postTipMessage).toHaveBeenNthCalledWith(3, 'ConfirmOnDevice');
4793
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
5380
4794
  1,
5381
- expect.objectContaining({ processedSize: 0, totalSize: 3 })
4795
+ expect.objectContaining({ processedSize: 0, totalSize: 2 })
5382
4796
  );
5383
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith(
4797
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenNthCalledWith(
5384
4798
  2,
5385
- expect.objectContaining({ processedSize: 2, totalSize: 3 })
4799
+ expect.objectContaining({ processedSize: 0, totalSize: 1 })
5386
4800
  );
5387
4801
  });
5388
4802
 
@@ -5396,7 +4810,8 @@ describe('Protocol V2 firmware update targets', () => {
5396
4810
 
5397
4811
  method.postTipMessage = jest.fn();
5398
4812
  method.postProgressMessage = jest.fn();
5399
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
4813
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
4814
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
5400
4815
  (method as any).device = stubDevice({
5401
4816
  getCommands: () => ({
5402
4817
  typedCall: jest.fn().mockResolvedValue({
@@ -5442,11 +4857,14 @@ describe('Protocol V2 firmware update targets', () => {
5442
4857
 
5443
4858
  method.postTipMessage = jest.fn();
5444
4859
  method.postProgressMessage = jest.fn();
5445
- (method as any).protocolV2CommonUpdateProcess = jest
4860
+ (method as any).protocolV2SourceUpdateProcess = jest
5446
4861
  .fn()
5447
4862
  .mockImplementation(params =>
5448
- Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
4863
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.source.size))
5449
4864
  );
4865
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
4866
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
4867
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5450
4868
  (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5451
4869
  (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
5452
4870
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
@@ -5661,42 +5079,33 @@ describe('Protocol V2 firmware update targets', () => {
5661
5079
  getFirmwareLatestReleaseSpy.mockRestore();
5662
5080
  });
5663
5081
 
5664
- test('does not download Pro2 firmware components that App did not select', async () => {
5082
+ test('maps remote Pro2 resource bundles to direct-write descriptors', () => {
5665
5083
  const method = new FirmwareUpdateV4({
5666
5084
  id: 1,
5667
5085
  payload: {
5668
5086
  method: 'firmwareUpdateV4',
5669
5087
  platform: 'web',
5088
+ targetsToUpdate: ['resource'],
5670
5089
  },
5671
5090
  });
5672
5091
  method.init();
5673
5092
 
5674
- const getSysResourceBinarySpy = jest
5675
- .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
5676
- .mockResolvedValue({ binary: new Uint8Array([1]).buffer });
5677
5093
  const getFirmwareLatestReleaseSpy = jest
5678
5094
  .spyOn(DataManager, 'getFirmwareLatestRelease')
5679
5095
  .mockReturnValue({
5680
5096
  required: false,
5681
5097
  version: [1, 0, 0],
5682
- url: 'https://example.com/applicationP1.pp.bin',
5683
- bootloaderVersion: [1, 0, 0],
5684
- upgradeType: 'payload-package-set',
5685
- installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
5686
- components: {
5687
- bootloader: {
5688
- target: 'BOOTLOADER',
5689
- url: 'https://example.com/bootloader.pp.bin',
5690
- },
5691
- applicationP1: {
5692
- target: 'APPLICATION_P1',
5693
- url: 'https://example.com/applicationP1.pp.bin',
5694
- },
5695
- applicationP2: {
5696
- target: 'APPLICATION_P2',
5697
- url: 'https://example.com/applicationP2.pp.bin',
5098
+ url: '',
5099
+ resourceBundles: [
5100
+ {
5101
+ name: 'images',
5102
+ url: 'https://example.com/images.okpkg',
5103
+ devicePath: 'VOL0:/resource/images/images.okpkg',
5104
+ version: [2, 0, 0],
5105
+ payloadHash: 'ab'.repeat(64),
5106
+ headerHash: 'cd'.repeat(64),
5698
5107
  },
5699
- },
5108
+ ],
5700
5109
  fingerprint: '',
5701
5110
  changelog: {
5702
5111
  'zh-CN': '',
@@ -5704,15 +5113,346 @@ describe('Protocol V2 firmware update targets', () => {
5704
5113
  },
5705
5114
  });
5706
5115
 
5707
- const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
5116
+ const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', {
5708
5117
  deviceType: 'pro2',
5709
5118
  firmwareVersion: '1.0.0',
5710
- bootloaderVersion: '1.0.0',
5711
5119
  capabilities: [],
5712
5120
  });
5713
5121
 
5714
- expect(remoteBinaries).toEqual({
5715
- bootloaderBinary: null,
5122
+ expect(bundles).toEqual([
5123
+ {
5124
+ name: 'images',
5125
+ binary: expect.any(ArrayBuffer),
5126
+ devicePath: 'vol0:/resource/images/images.okpkg',
5127
+ url: 'https://example.com/images.okpkg',
5128
+ version: [2, 0, 0],
5129
+ payloadHash: 'ab'.repeat(64),
5130
+ headerHash: 'cd'.repeat(64),
5131
+ },
5132
+ ]);
5133
+ expect(bundles[0].binary.byteLength).toBe(0);
5134
+
5135
+ getFirmwareLatestReleaseSpy.mockRestore();
5136
+ });
5137
+
5138
+ test('rejects remote RESC bundle paths before download or bootloader entry', async () => {
5139
+ const method = new FirmwareUpdateV4({
5140
+ id: 1,
5141
+ payload: {
5142
+ method: 'firmwareUpdateV4',
5143
+ platform: 'web',
5144
+ targetsToUpdate: ['resource'],
5145
+ },
5146
+ });
5147
+ method.init();
5148
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
5149
+
5150
+ const getSysResourceBinarySpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
5151
+ const getFirmwareLatestReleaseSpy = jest
5152
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
5153
+ .mockReturnValue({
5154
+ required: false,
5155
+ version: [1, 0, 0],
5156
+ url: '',
5157
+ resourceBundles: [
5158
+ {
5159
+ name: 'images',
5160
+ url: 'https://example.com/images.okpkg',
5161
+ devicePath: 'vol0:/resource/../images.okpkg',
5162
+ },
5163
+ ],
5164
+ fingerprint: '',
5165
+ changelog: {
5166
+ 'zh-CN': '',
5167
+ 'en-US': '',
5168
+ },
5169
+ });
5170
+
5171
+ (method as any).device = stubDevice({
5172
+ originalDescriptor: { protocolType: 'V2' },
5173
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
5174
+ });
5175
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
5176
+ bootloaderBinary: null,
5177
+ fwBinaryMap: [],
5178
+ });
5179
+ (method as any).enterProtocolV2BootloaderMode = jest.fn();
5180
+ method.postTipMessage = jest.fn();
5181
+
5182
+ await expect(method.run()).rejects.toThrow('resourceBundles[0].devicePath');
5183
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
5184
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
5185
+
5186
+ getSysResourceBinarySpy.mockRestore();
5187
+ getFirmwareLatestReleaseSpy.mockRestore();
5188
+ });
5189
+
5190
+ test('rejects prepared RESC bundle paths before opening an artifact source', async () => {
5191
+ const method = new FirmwareUpdateV4({
5192
+ id: 1,
5193
+ payload: {
5194
+ method: 'firmwareUpdateV4',
5195
+ },
5196
+ });
5197
+ const openPreparedSource = jest.fn();
5198
+ (method as any).params = {
5199
+ targetsToUpdate: ['resource'],
5200
+ resourceBundleArtifacts: [
5201
+ {
5202
+ name: 'images',
5203
+ artifact: {
5204
+ artifactRef: `fw:${'a'.repeat(64)}`,
5205
+ size: 1,
5206
+ sha256: 'a'.repeat(64),
5207
+ },
5208
+ },
5209
+ ],
5210
+ };
5211
+ (method as any).openProtocolV2PreparedSource = openPreparedSource;
5212
+ const getFirmwareLatestReleaseSpy = jest
5213
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
5214
+ .mockReturnValue({
5215
+ required: false,
5216
+ version: [1, 0, 0],
5217
+ url: '',
5218
+ resourceBundles: [
5219
+ {
5220
+ name: 'images',
5221
+ url: 'https://example.com/images.okpkg',
5222
+ devicePath: 'vol0:/resource/../images.okpkg',
5223
+ },
5224
+ ],
5225
+ fingerprint: '',
5226
+ changelog: {
5227
+ 'zh-CN': '',
5228
+ 'en-US': '',
5229
+ },
5230
+ });
5231
+
5232
+ await expect(
5233
+ (method as any).prepareProtocolV2ResourceSources('universal', {
5234
+ deviceType: 'pro2',
5235
+ })
5236
+ ).rejects.toThrow('resourceBundles[].devicePath');
5237
+ expect(openPreparedSource).not.toHaveBeenCalled();
5238
+
5239
+ getFirmwareLatestReleaseSpy.mockRestore();
5240
+ });
5241
+
5242
+ test('downloads and validates remote RESC bundles before entering bootloader', async () => {
5243
+ const method = new FirmwareUpdateV4({
5244
+ id: 1,
5245
+ payload: {
5246
+ method: 'firmwareUpdateV4',
5247
+ platform: 'web',
5248
+ targetsToUpdate: ['resource'],
5249
+ },
5250
+ });
5251
+ method.init();
5252
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
5253
+
5254
+ const order: string[] = [];
5255
+ const resourceBinary = buildOkppHeader().buffer as ArrayBuffer;
5256
+ const getSysResourceBinarySpy = jest
5257
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
5258
+ .mockImplementation(() => {
5259
+ order.push('download');
5260
+ return Promise.resolve({ binary: resourceBinary });
5261
+ });
5262
+ const getFirmwareLatestReleaseSpy = jest
5263
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
5264
+ .mockReturnValue({
5265
+ required: false,
5266
+ version: [1, 0, 0],
5267
+ url: '',
5268
+ resourceBundles: [
5269
+ {
5270
+ name: 'images',
5271
+ url: 'https://example.com/images.okpkg',
5272
+ devicePath: ' VOL0:/resource/images/images.okpkg ',
5273
+ },
5274
+ ],
5275
+ fingerprint: '',
5276
+ changelog: {
5277
+ 'zh-CN': '',
5278
+ 'en-US': '',
5279
+ },
5280
+ });
5281
+
5282
+ (method as any).device = stubDevice({
5283
+ originalDescriptor: { protocolType: 'V2' },
5284
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
5285
+ });
5286
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
5287
+ bootloaderBinary: null,
5288
+ fwBinaryMap: [],
5289
+ });
5290
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockImplementation(() => {
5291
+ order.push('bootloader');
5292
+ return Promise.resolve(true);
5293
+ });
5294
+ const executeProtocolV2UpdateSpy = jest.spyOn(method as any, 'executeProtocolV2Update');
5295
+ (method as any).executeProtocolV2TransferPhase = jest.fn().mockResolvedValue(undefined);
5296
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue(undefined);
5297
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
5298
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
5299
+ bootloaderVersion: '1.0.0',
5300
+ bleVersion: '0.0.0',
5301
+ firmwareVersion: '1.0.0',
5302
+ });
5303
+ method.postTipMessage = jest.fn();
5304
+
5305
+ await method.run();
5306
+
5307
+ expect(order).toEqual(['download', 'bootloader']);
5308
+ expect(executeProtocolV2UpdateSpy).toHaveBeenCalledWith({
5309
+ bootloaderBinary: null,
5310
+ fwBinaryMap: [],
5311
+ resourceBundles: [
5312
+ {
5313
+ name: 'images',
5314
+ binary: resourceBinary,
5315
+ devicePath: 'vol0:/resource/images/images.okpkg',
5316
+ url: 'https://example.com/images.okpkg',
5317
+ },
5318
+ ],
5319
+ });
5320
+
5321
+ executeProtocolV2UpdateSpy.mockRestore();
5322
+ getSysResourceBinarySpy.mockRestore();
5323
+ getFirmwareLatestReleaseSpy.mockRestore();
5324
+ });
5325
+
5326
+ test('rejects a remote RESC bundle without an OKPP header when metadata is absent', async () => {
5327
+ const method = new FirmwareUpdateV4({
5328
+ id: 1,
5329
+ payload: {
5330
+ method: 'firmwareUpdateV4',
5331
+ },
5332
+ });
5333
+ method.init();
5334
+ const getSysResourceBinarySpy = jest
5335
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
5336
+ .mockResolvedValue({ binary: new Uint8Array([1, 2, 3]).buffer });
5337
+
5338
+ await expect(
5339
+ (method as any).materializeProtocolV2ResourceBundles([
5340
+ {
5341
+ name: 'images',
5342
+ binary: new ArrayBuffer(0),
5343
+ devicePath: 'vol0:/resource/images/images.okpkg',
5344
+ url: 'https://example.com/images.okpkg',
5345
+ },
5346
+ ])
5347
+ ).rejects.toThrow('Invalid Protocol V2 RESC bundle header: images');
5348
+
5349
+ getSysResourceBinarySpy.mockRestore();
5350
+ });
5351
+
5352
+ test('rejects an empty remote RESC bundle before entering bootloader', async () => {
5353
+ const method = new FirmwareUpdateV4({
5354
+ id: 1,
5355
+ payload: {
5356
+ method: 'firmwareUpdateV4',
5357
+ platform: 'web',
5358
+ targetsToUpdate: ['resource'],
5359
+ },
5360
+ });
5361
+ method.init();
5362
+ (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
5363
+
5364
+ const getSysResourceBinarySpy = jest
5365
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
5366
+ .mockResolvedValue({ binary: new ArrayBuffer(0) });
5367
+ const getFirmwareLatestReleaseSpy = jest
5368
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
5369
+ .mockReturnValue({
5370
+ required: false,
5371
+ version: [1, 0, 0],
5372
+ url: '',
5373
+ resourceBundles: [
5374
+ {
5375
+ name: 'images',
5376
+ url: 'https://example.com/images.okpkg',
5377
+ devicePath: 'vol0:/resource/images/images.okpkg',
5378
+ },
5379
+ ],
5380
+ fingerprint: '',
5381
+ changelog: {
5382
+ 'zh-CN': '',
5383
+ 'en-US': '',
5384
+ },
5385
+ });
5386
+ (method as any).device = stubDevice({
5387
+ originalDescriptor: { protocolType: 'V2' },
5388
+ features: { deviceType: 'pro2', firmwareVersion: '1.0.0', capabilities: [] },
5389
+ });
5390
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue({
5391
+ bootloaderBinary: null,
5392
+ fwBinaryMap: [],
5393
+ });
5394
+ (method as any).enterProtocolV2BootloaderMode = jest.fn();
5395
+ method.postTipMessage = jest.fn();
5396
+
5397
+ await expect(method.run()).rejects.toThrow('Protocol V2 RESC bundle is empty: images');
5398
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
5399
+
5400
+ getSysResourceBinarySpy.mockRestore();
5401
+ getFirmwareLatestReleaseSpy.mockRestore();
5402
+ });
5403
+
5404
+ test('does not download Pro2 firmware components that App did not select', async () => {
5405
+ const method = new FirmwareUpdateV4({
5406
+ id: 1,
5407
+ payload: {
5408
+ method: 'firmwareUpdateV4',
5409
+ platform: 'web',
5410
+ },
5411
+ });
5412
+ method.init();
5413
+
5414
+ const getSysResourceBinarySpy = jest
5415
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
5416
+ .mockResolvedValue({ binary: new Uint8Array([1]).buffer });
5417
+ const getFirmwareLatestReleaseSpy = jest
5418
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
5419
+ .mockReturnValue({
5420
+ required: false,
5421
+ version: [1, 0, 0],
5422
+ url: 'https://example.com/applicationP1.pp.bin',
5423
+ bootloaderVersion: [1, 0, 0],
5424
+ upgradeType: 'payload-package-set',
5425
+ installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
5426
+ components: {
5427
+ bootloader: {
5428
+ target: 'BOOTLOADER',
5429
+ url: 'https://example.com/bootloader.pp.bin',
5430
+ },
5431
+ applicationP1: {
5432
+ target: 'APPLICATION_P1',
5433
+ url: 'https://example.com/applicationP1.pp.bin',
5434
+ },
5435
+ applicationP2: {
5436
+ target: 'APPLICATION_P2',
5437
+ url: 'https://example.com/applicationP2.pp.bin',
5438
+ },
5439
+ },
5440
+ fingerprint: '',
5441
+ changelog: {
5442
+ 'zh-CN': '',
5443
+ 'en-US': '',
5444
+ },
5445
+ });
5446
+
5447
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
5448
+ deviceType: 'pro2',
5449
+ firmwareVersion: '1.0.0',
5450
+ bootloaderVersion: '1.0.0',
5451
+ capabilities: [],
5452
+ });
5453
+
5454
+ expect(remoteBinaries).toEqual({
5455
+ bootloaderBinary: null,
5716
5456
  fwBinaryMap: [],
5717
5457
  installItems: [],
5718
5458
  });
@@ -5761,7 +5501,6 @@ describe('Protocol V2 firmware update targets', () => {
5761
5501
 
5762
5502
  await method.run();
5763
5503
 
5764
- expect(forceReloadDataSpy).toHaveBeenCalledTimes(1);
5765
5504
  expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
5766
5505
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
5767
5506
  bootloaderBinary: remoteBinaries.bootloaderBinary,
@@ -5769,36 +5508,6 @@ describe('Protocol V2 firmware update targets', () => {
5769
5508
  });
5770
5509
  });
5771
5510
 
5772
- test('stops a remote update before reboot when the latest config cannot be refreshed', async () => {
5773
- const method = new FirmwareUpdateV4({
5774
- id: 1,
5775
- payload: {
5776
- method: 'firmwareUpdateV4',
5777
- platform: 'web',
5778
- },
5779
- });
5780
- method.init();
5781
- (method as any).captureProtocolV2PhysicalIdentity = jest.fn().mockResolvedValue(undefined);
5782
- (method as any).device = stubDevice({
5783
- originalDescriptor: { protocolType: 'V2' },
5784
- features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
5785
- });
5786
- forceReloadDataSpy.mockRejectedValue(
5787
- ERRORS.TypedError(HardwareErrorCode.NetworkError, 'latest config unavailable')
5788
- );
5789
- (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
5790
- (method as any).enterProtocolV2BootloaderMode = jest.fn();
5791
- method.postTipMessage = jest.fn();
5792
-
5793
- await expect(method.run()).rejects.toMatchObject({
5794
- errorCode: HardwareErrorCode.NetworkError,
5795
- message: 'latest config unavailable',
5796
- });
5797
-
5798
- expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
5799
- expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
5800
- });
5801
-
5802
5511
  test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
5803
5512
  const method = new FirmwareUpdateV4({
5804
5513
  id: 1,
@@ -5835,14 +5544,14 @@ describe('Protocol V2 firmware update targets', () => {
5835
5544
  });
5836
5545
  });
5837
5546
 
5838
- test('treats manual resource files as explicit payload without remote firmware auto-fill', async () => {
5547
+ test('treats manual resource bundles as explicit payload without remote firmware auto-fill', async () => {
5839
5548
  const resourceBundle = new Uint8Array([1, 2, 3]).buffer;
5840
5549
  const method = new FirmwareUpdateV4({
5841
5550
  id: 1,
5842
5551
  payload: {
5843
5552
  method: 'firmwareUpdateV4',
5844
5553
  platform: 'web',
5845
- resourceFiles: [
5554
+ resourceBundleFiles: [
5846
5555
  {
5847
5556
  binary: resourceBundle,
5848
5557
  devicePath: ' VOL0:/resource/images/images.okpkg ',
@@ -5890,7 +5599,7 @@ describe('Protocol V2 firmware update targets', () => {
5890
5599
  payload: {
5891
5600
  method: 'firmwareUpdateV4',
5892
5601
  platform: 'web',
5893
- resourceFiles: [
5602
+ resourceBundleFiles: [
5894
5603
  {
5895
5604
  binary: new Uint8Array([1, 2, 3]).buffer,
5896
5605
  devicePath: 'vol2:/resource/images/images.okpkg',
@@ -5909,7 +5618,7 @@ describe('Protocol V2 firmware update targets', () => {
5909
5618
  (method as any).enterProtocolV2BootloaderMode = jest.fn();
5910
5619
  method.postTipMessage = jest.fn();
5911
5620
 
5912
- await expect(method.run()).rejects.toThrow('resourceFiles[0].devicePath');
5621
+ await expect(method.run()).rejects.toThrow('resourceBundleFiles[0].devicePath');
5913
5622
  expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
5914
5623
  expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
5915
5624
  });
@@ -5924,7 +5633,10 @@ describe('Protocol V2 firmware update targets', () => {
5924
5633
 
5925
5634
  method.postTipMessage = jest.fn();
5926
5635
  method.postProgressMessage = jest.fn();
5927
- (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3);
5636
+ (method as any).protocolV2SourceUpdateProcess = jest.fn().mockResolvedValue(3);
5637
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(undefined);
5638
+ (method as any).completeProtocolV2FinalVerification = jest.fn().mockResolvedValue({});
5639
+ (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined);
5928
5640
  (method as any).protocolV2StartFirmwareUpdate = jest.fn();
5929
5641
  (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn();
5930
5642
 
@@ -5940,7 +5652,7 @@ describe('Protocol V2 firmware update targets', () => {
5940
5652
  fwBinaryMap: [],
5941
5653
  });
5942
5654
 
5943
- expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1);
5655
+ expect((method as any).protocolV2SourceUpdateProcess).toHaveBeenCalledTimes(1);
5944
5656
  expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled();
5945
5657
  expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled();
5946
5658
  });
@@ -5968,17 +5680,22 @@ describe('Protocol V2 firmware update targets', () => {
5968
5680
 
5969
5681
  (method as any).device = stubDevice({
5970
5682
  getCommands: () => ({ typedCall }),
5971
- getCurrentDeviceType: () => 'pro2',
5972
- toMessageObject: () => ({ connectId: 'firmware-device' }),
5973
5683
  });
5974
- method.postMessage = jest.fn();
5684
+ method.postProgressMessage = jest.fn();
5975
5685
 
5976
- await (method as any).protocolV2CommonUpdateProcess({
5977
- payload: new Uint8Array(4097).buffer,
5978
- filePath: 'vol1:firmware.bin',
5979
- processedSize: 0,
5980
- totalSize: 4097,
5686
+ const source = await openFirmwareByteSource({
5687
+ binary: new Uint8Array(4097).buffer,
5981
5688
  });
5689
+ try {
5690
+ await (method as any).protocolV2SourceUpdateProcess({
5691
+ source,
5692
+ filePath: 'vol1:firmware.bin',
5693
+ processedSize: 0,
5694
+ totalSize: 4097,
5695
+ });
5696
+ } finally {
5697
+ await source?.close();
5698
+ }
5982
5699
 
5983
5700
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
5984
5701
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
@@ -5986,17 +5703,6 @@ describe('Protocol V2 firmware update targets', () => {
5986
5703
  expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
5987
5704
  expect(writePayloads.every(payload => payload.append === false)).toBe(true);
5988
5705
  expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
5989
- expect(method.postMessage).toHaveBeenLastCalledWith({
5990
- event: 'UI_EVENT',
5991
- type: UI_REQUEST.FIRMWARE_PROGRESS,
5992
- payload: expect.objectContaining({
5993
- progress: 99,
5994
- progressType: 'transferData',
5995
- transferredBytes: 4097,
5996
- totalBytes: 4097,
5997
- elapsedMs: expect.any(Number),
5998
- }),
5999
- });
6000
5706
  });
6001
5707
 
6002
5708
  test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => {
@@ -6046,12 +5752,16 @@ describe('Protocol V2 firmware update targets', () => {
6046
5752
  method.postProgressMessage = jest.fn();
6047
5753
 
6048
5754
  try {
6049
- await (method as any).protocolV2CommonUpdateProcess({
6050
- payload: new Uint8Array(8001).buffer,
5755
+ const source = await openFirmwareByteSource({
5756
+ binary: new Uint8Array(8001).buffer,
5757
+ });
5758
+ await (method as any).protocolV2SourceUpdateProcess({
5759
+ source,
6051
5760
  filePath: 'vol0:/firmware.bin',
6052
5761
  processedSize: 0,
6053
5762
  totalSize: 8001,
6054
5763
  });
5764
+ await source?.close();
6055
5765
  } finally {
6056
5766
  setTimeoutSpy.mockRestore();
6057
5767
  }
@@ -6086,16 +5796,21 @@ describe('Protocol V2 firmware update targets', () => {
6086
5796
  });
6087
5797
  method.postProgressMessage = jest.fn();
6088
5798
  method.postTipMessage = jest.fn();
5799
+ (method as any).recoverProtocolV2FileTransfer = jest.fn().mockResolvedValue(undefined);
6089
5800
 
5801
+ const source = await openFirmwareByteSource({
5802
+ binary: new Uint8Array(4097).buffer,
5803
+ });
6090
5804
  await expect(
6091
- (method as any).protocolV2WriteWholeFile({
6092
- payload: new Uint8Array(4097).buffer,
5805
+ (method as any).protocolV2SourceUpdateProcess({
5806
+ source,
6093
5807
  filePath: 'vol0:/firmware.bin',
6094
5808
  processedSize: 0,
6095
5809
  totalSize: 4097,
6096
5810
  })
6097
5811
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.EmmcFileWriteFirmwareError });
6098
- expect(typedCall).toHaveBeenCalledTimes(2);
5812
+ await source?.close();
5813
+ expect(typedCall).toHaveBeenCalledTimes(6);
6099
5814
  });
6100
5815
 
6101
5816
  test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
@@ -6128,21 +5843,24 @@ describe('Protocol V2 firmware update targets', () => {
6128
5843
  });
6129
5844
  method.postProgressMessage = jest.fn();
6130
5845
 
6131
- await (method as any).protocolV2CommonUpdateProcess({
6132
- payload: new Uint8Array(1801).buffer,
6133
- filePath: 'vol1:ble-firmware.bin',
6134
- processedSize: 0,
6135
- totalSize: 1801,
5846
+ const source = await openFirmwareByteSource({
5847
+ binary: new Uint8Array(1801).buffer,
6136
5848
  });
5849
+ try {
5850
+ await (method as any).protocolV2SourceUpdateProcess({
5851
+ source,
5852
+ filePath: 'vol1:ble-firmware.bin',
5853
+ processedSize: 0,
5854
+ totalSize: 1801,
5855
+ });
5856
+ } finally {
5857
+ await source?.close();
5858
+ }
6137
5859
 
6138
5860
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
6139
5861
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
6140
5862
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
6141
5863
  expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
6142
- expect(typedCall.mock.calls.map(call => call[3])).toEqual([
6143
- { writeWithResponse: true },
6144
- { writeWithResponse: true },
6145
- ]);
6146
5864
  });
6147
5865
 
6148
5866
  test('ends device confirmation and starts install progress only after Protocol V2 ACK', async () => {
@@ -6254,9 +5972,9 @@ describe('Protocol V2 firmware update method', () => {
6254
5972
  const typedCall = jest
6255
5973
  .fn()
6256
5974
  .mockRejectedValue(
6257
- new DOMException(
6258
- "Failed to execute 'open' on 'USBDevice': The device was disconnected",
6259
- 'NotFoundError'
5975
+ Object.assign(
5976
+ new Error("Failed to execute 'open' on 'USBDevice': The device was disconnected"),
5977
+ { name: 'NotFoundError' }
6260
5978
  )
6261
5979
  );
6262
5980
 
@@ -6310,7 +6028,7 @@ describe('Protocol V2 firmware update method', () => {
6310
6028
  expect(typedCall).not.toHaveBeenCalled();
6311
6029
  });
6312
6030
 
6313
- test.each([0, 1, 2])(
6031
+ test.each([0, 2])(
6314
6032
  'rejects firmware target %s because the Pro2 bootloader cannot install it',
6315
6033
  async targetId => {
6316
6034
  const typedCall = jest.fn();
@@ -6343,8 +6061,8 @@ describe('Protocol V2 firmware update method', () => {
6343
6061
  targets: [
6344
6062
  {
6345
6063
  target_id: undefined,
6346
- targetId: 3,
6347
- path: 'vol0:bootloader.okpkg',
6064
+ targetId: 1,
6065
+ path: 'vol0:resource.crate.okpkg',
6348
6066
  },
6349
6067
  ],
6350
6068
  } as any,
@@ -6356,7 +6074,7 @@ describe('Protocol V2 firmware update method', () => {
6356
6074
 
6357
6075
  await method.run();
6358
6076
  expect(typedCall.mock.calls[0][2]).toEqual({
6359
- targets: [{ target_id: 3, path: 'vol0:bootloader.okpkg' }],
6077
+ targets: [{ target_id: 1, path: 'vol0:resource.crate.okpkg' }],
6360
6078
  });
6361
6079
  });
6362
6080
 
@@ -6421,63 +6139,20 @@ describe('Protocol V2 firmware update method', () => {
6421
6139
  });
6422
6140
 
6423
6141
  describe('Protocol V2 firmware reconnect identity', () => {
6424
- const createResourceFilesystemTypedCall = (
6425
- stable: Array<{ type: string; size: number; headerHash: string }>,
6426
- missingTypes: string[] = []
6427
- ) => {
6428
- const missing = new Set(missingTypes);
6429
- const resourceByPath = new Map(
6430
- stable.map(resource => [
6431
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS[
6432
- resource.type as keyof typeof PROTOCOL_V2_RESOURCE_DEVICE_PATHS
6433
- ],
6434
- resource,
6435
- ])
6436
- );
6437
- return jest.fn((requestType: string, _responseType: string, payload: Record<string, any>) => {
6438
- if (requestType === 'ResourceInventoryGet') {
6439
- throw new Error('ResourceInventoryGet is unavailable on released firmware');
6440
- }
6441
- if (requestType === 'FilesystemPathInfoQuery') {
6442
- const resource = resourceByPath.get(payload.path);
6443
- const exists = Boolean(resource && !missing.has(resource.type));
6444
- return {
6445
- message: {
6446
- exist: exists,
6447
- directory: false,
6448
- size: exists ? resource?.size : 0,
6449
- },
6450
- };
6451
- }
6452
- if (requestType === 'FilesystemFileRead') {
6453
- const resource = resourceByPath.get(payload.file.path);
6454
- if (!resource || missing.has(resource.type)) throw new Error('missing resource');
6455
- const header = new Uint8Array(0x52a0);
6456
- const view = new DataView(header.buffer);
6457
- 'OKPP'.split('').forEach((char, index) => {
6458
- header[index] = char.charCodeAt(0);
6459
- });
6460
- 'RESC'.split('').forEach((char, index) => {
6461
- header[0x08 + index] = char.charCodeAt(0);
6462
- });
6463
- view.setUint32(0x0c, header.byteLength, true);
6464
- for (let index = 0; index < resource.headerHash.length / 2; index++) {
6465
- header[0x240 + index] = Number.parseInt(
6466
- resource.headerHash.slice(index * 2, index * 2 + 2),
6467
- 16
6468
- );
6469
- }
6470
- const offset = Number(payload.file.offset);
6471
- const chunkLength = Number(payload.chunk_len);
6472
- return {
6473
- message: {
6474
- data: header.slice(offset, offset + chunkLength),
6475
- },
6476
- };
6477
- }
6478
- throw new Error(`Unexpected request: ${requestType}`);
6142
+ test('keeps the host plan identity as the V4 reconnect anchor', () => {
6143
+ const method = new FirmwareUpdateV4({
6144
+ id: 1,
6145
+ payload: {
6146
+ method: 'firmwareUpdateV4',
6147
+ platform: 'native',
6148
+ expectedDeviceId: 'persisted-device-id',
6149
+ },
6479
6150
  });
6480
- };
6151
+
6152
+ method.init();
6153
+
6154
+ expect((method as any).params.expectedDeviceId).toBe('persisted-device-id');
6155
+ });
6481
6156
 
6482
6157
  test('rejects a different physical serial before firmware transfer resumes', () => {
6483
6158
  expect(() => assertProtocolV2ReconnectIdentity('expected-serial', 'other-serial')).toThrow(
@@ -6488,15 +6163,15 @@ describe('Protocol V2 firmware reconnect identity', () => {
6488
6163
  ).not.toThrow();
6489
6164
  });
6490
6165
 
6491
- test('requires the same descriptor path when either physical serial is unavailable', () => {
6492
- expect(() =>
6493
- assertProtocolV2ReconnectIdentity('expected-serial', undefined, 'usb-path', 'usb-path')
6494
- ).not.toThrow();
6495
- expect(() =>
6496
- assertProtocolV2ReconnectIdentity(undefined, 'actual-serial', 'usb-path', 'other-path')
6497
- ).toThrow('identity unavailable');
6166
+ test('fails closed when the physical serial is unavailable', () => {
6167
+ expect(() => assertProtocolV2ReconnectIdentity('expected-serial', undefined)).toThrow(
6168
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6169
+ );
6170
+ expect(() => assertProtocolV2ReconnectIdentity(undefined, 'actual-serial')).toThrow(
6171
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6172
+ );
6498
6173
  expect(() => assertProtocolV2ReconnectIdentity(undefined, undefined)).toThrow(
6499
- 'identity unavailable'
6174
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotFound })
6500
6175
  );
6501
6176
  });
6502
6177
 
@@ -6515,7 +6190,6 @@ describe('Protocol V2 firmware reconnect identity', () => {
6515
6190
  },
6516
6191
  });
6517
6192
  (method as any).device = stubDevice({
6518
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
6519
6193
  features: { deviceId: 'wallet-derived-id' },
6520
6194
  getCommands: () => ({ typedCall }),
6521
6195
  });
@@ -6531,7 +6205,7 @@ describe('Protocol V2 firmware reconnect identity', () => {
6531
6205
  );
6532
6206
  });
6533
6207
 
6534
- test('allows firmware update startup when DeviceInfo has no physical serial', async () => {
6208
+ test('rejects firmware update startup when DeviceInfo has no physical serial', async () => {
6535
6209
  const method = new FirmwareUpdateV4({
6536
6210
  id: 1,
6537
6211
  payload: { method: 'firmwareUpdateV4' },
@@ -6541,204 +6215,42 @@ describe('Protocol V2 firmware reconnect identity', () => {
6541
6215
  message: { protocol_version: 1, hw: {} },
6542
6216
  });
6543
6217
  (method as any).device = stubDevice({
6544
- originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
6545
6218
  getCommands: () => ({ typedCall }),
6546
6219
  });
6547
6220
 
6548
- await expect((method as any).captureProtocolV2PhysicalIdentity()).resolves.toBeUndefined();
6549
- expect((method as any).protocolV2ExpectedSerialNumber).toBeUndefined();
6550
- expect((method as any).protocolV2ExpectedPath).toBeDefined();
6221
+ await expect((method as any).captureProtocolV2PhysicalIdentity()).rejects.toMatchObject({
6222
+ errorCode: HardwareErrorCode.DeviceNotFound,
6223
+ });
6551
6224
  });
6552
6225
 
6553
- test('uses filesystem reads instead of ResourceInventoryGet for resource comparison', async () => {
6226
+ test('uses the BLE read limit when reading an existing firmware bundle header', async () => {
6554
6227
  const method = new FirmwareUpdateV4({
6555
6228
  id: 1,
6556
6229
  payload: {
6557
6230
  method: 'firmwareUpdateV4',
6558
- platform: 'web',
6559
- targetsToUpdate: ['resource'],
6231
+ platform: 'native',
6232
+ chunkSize: 1800,
6560
6233
  },
6561
6234
  });
6562
6235
  method.init();
6563
- const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
6564
- (type, index) => ({
6565
- type,
6566
- url: `https://example.com/${type}.okpkg`,
6567
- size: 0x52a0 + index + 1,
6568
- fileHash: 'a'.repeat(64),
6569
- headerHash: index.toString(16).padStart(128, '0'),
6570
- })
6571
- );
6572
- const typedCall = createResourceFilesystemTypedCall(stable);
6573
- (method as any).device = stubDevice({
6574
- getCommands: () => ({ typedCall }),
6575
- getCurrentDeviceType: () => 'pro2',
6576
- });
6577
- const resourcesSpy = jest
6578
- .spyOn(DataManager, 'getProtocolV2Resources')
6579
- .mockReturnValue(stable as any);
6580
- (method as any).downloadProtocolV2Resource = jest.fn();
6581
-
6582
- await expect((method as any).prepareProtocolV2ResourceBundles()).resolves.toEqual([]);
6583
- expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
6584
- expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemFileRead')).toBe(true);
6585
- expect((method as any).downloadProtocolV2Resource).not.toHaveBeenCalled();
6586
- resourcesSpy.mockRestore();
6587
- });
6588
-
6589
- test('downloads only changed resources from loader inventory', async () => {
6590
- const method = new FirmwareUpdateV4({
6591
- id: 1,
6592
- payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
6593
- });
6594
- method.init();
6595
- const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
6596
- (type, index) => ({
6597
- type,
6598
- url: `https://example.com/${type}.okpkg`,
6599
- size: 0x52a0 + index + 1,
6600
- fileHash: 'a'.repeat(64),
6601
- headerHash: index.toString(16).padStart(128, '0'),
6602
- })
6603
- );
6604
- const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
6605
- (method as any).device = stubDevice({
6606
- getCommands: () => ({ typedCall }),
6607
- getCurrentDeviceType: () => 'pro2',
6608
- });
6609
- const resourcesSpy = jest
6610
- .spyOn(DataManager, 'getProtocolV2Resources')
6611
- .mockReturnValue(stable as any);
6612
- (method as any).downloadProtocolV2Resource = jest.fn(resource =>
6613
- Promise.resolve({
6614
- name: `${resource.type}.okpkg`,
6615
- binary: new ArrayBuffer(resource.size),
6616
- devicePath: `vol0:/${resource.type}.okpkg`,
6617
- })
6618
- );
6619
-
6620
- const bundles = await (method as any).prepareProtocolV2ResourceBundles();
6621
-
6622
- expect(bundles).toHaveLength(1);
6623
- expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
6624
- resourcesSpy.mockRestore();
6625
- });
6626
-
6627
- test('uses filesystem inventory for incremental Bootloader recovery', async () => {
6628
- const method = new FirmwareUpdateV4({
6629
- id: 1,
6630
- payload: { method: 'firmwareUpdateV4', platform: 'web', targetsToUpdate: ['resource'] },
6236
+ const readChunkLengths: number[] = [];
6237
+ const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
6238
+ if (requestType === 'FilesystemPathInfoQuery') {
6239
+ return Promise.resolve({ message: { exist: true, directory: false, size: 0x52a0 } });
6240
+ }
6241
+ if (requestType === 'FilesystemFileRead') {
6242
+ readChunkLengths.push(request.chunk_len);
6243
+ return Promise.resolve({ message: { data: new Uint8Array(request.chunk_len) } });
6244
+ }
6245
+ throw new Error(`Unexpected request: ${requestType}`);
6631
6246
  });
6632
- method.init();
6633
- const stable = ['images', 'animation', 'wallpaper', 'translations', 'roobert', 'noto'].map(
6634
- (type, index) => ({
6635
- type,
6636
- url: `https://example.com/${type}.okpkg`,
6637
- size: 0x52a0 + index + 1,
6638
- fileHash: 'a'.repeat(64),
6639
- headerHash: index.toString(16).padStart(128, '0'),
6640
- })
6641
- );
6642
- const typedCall = createResourceFilesystemTypedCall(stable, ['images']);
6643
6247
  (method as any).device = stubDevice({
6644
6248
  getCommands: () => ({ typedCall }),
6645
- getCurrentDeviceType: () => 'pro2',
6646
- });
6647
- const resourcesSpy = jest
6648
- .spyOn(DataManager, 'getProtocolV2Resources')
6649
- .mockReturnValue(stable as any);
6650
- (method as any).downloadProtocolV2Resource = jest.fn(resource =>
6651
- Promise.resolve({
6652
- name: `${resource.type}.okpkg`,
6653
- binary: new ArrayBuffer(resource.size),
6654
- devicePath: `vol0:/${resource.type}.okpkg`,
6655
- })
6656
- );
6657
-
6658
- const bundles = await (method as any).prepareProtocolV2ResourceBundles();
6659
-
6660
- expect(bundles).toHaveLength(1);
6661
- expect(typedCall.mock.calls.some(call => call[0] === 'ResourceInventoryGet')).toBe(false);
6662
- expect(typedCall.mock.calls.some(call => call[0] === 'FilesystemPathInfoQuery')).toBe(true);
6663
- expect((method as any).downloadProtocolV2Resource).toHaveBeenCalledWith(stable[0]);
6664
- resourcesSpy.mockRestore();
6665
- });
6666
-
6667
- test('does not resolve boot resources unless the optional target is selected', async () => {
6668
- const method = new FirmwareUpdateV4({
6669
- id: 1,
6670
- payload: { method: 'firmwareUpdateV4', platform: 'web' },
6671
- });
6672
- method.init();
6673
- const configSpy = jest.spyOn(DataManager, 'getProtocolV2BootResources');
6674
- const downloadSpy = jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary');
6675
-
6676
- await expect((method as any).prepareProtocolV2BootResources()).resolves.toBeUndefined();
6677
-
6678
- expect(configSpy).not.toHaveBeenCalled();
6679
- expect(downloadSpy).not.toHaveBeenCalled();
6680
- });
6681
-
6682
- test('downloads and maps selected boot resources as direct RES files', async () => {
6683
- const bytes = new Uint8Array([1, 2, 3, 4]);
6684
- const binary = bytes.buffer as ArrayBuffer;
6685
- const fileHash = Array.from(sha256(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
6686
- const resource = {
6687
- required: false as const,
6688
- target: 'RES' as const,
6689
- files: [
6690
- {
6691
- name: 'bootloader_crest.bin',
6692
- url: 'https://example.com/bootloader_crest.bin',
6693
- devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
6694
- size: bytes.byteLength,
6695
- fileHash,
6696
- },
6697
- ],
6698
- };
6699
- const method = new FirmwareUpdateV4({
6700
- id: 1,
6701
- payload: {
6702
- method: 'firmwareUpdateV4',
6703
- platform: 'web',
6704
- targetsToUpdate: ['boot_resources'],
6705
- },
6706
- });
6707
- method.init();
6708
- (method as any).device = stubDevice({ getCurrentDeviceType: () => 'pro2' });
6709
- jest.spyOn(DataManager, 'getProtocolV2BootResources').mockReturnValue(resource);
6710
- jest.spyOn(firmwareBinaryApi, 'getSysResourceBinary').mockResolvedValue({ binary });
6711
-
6712
- await expect((method as any).prepareProtocolV2BootResources()).resolves.toEqual([
6713
- {
6714
- name: 'bootloader_crest.bin',
6715
- binary,
6716
- devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
6717
- },
6718
- ]);
6719
- });
6720
-
6721
- test('rejects a manually supplied resource file when its manifest hash does not match', () => {
6722
- const method = new FirmwareUpdateV4({
6723
- id: 1,
6724
- payload: {
6725
- method: 'firmwareUpdateV4',
6726
- platform: 'web',
6727
- resourceFiles: [
6728
- {
6729
- binary: new Uint8Array([1, 2, 3]).buffer,
6730
- devicePath: 'vol0:/assets/loaders/boot.staging/graphics/bootloader_crest.bin',
6731
- size: 3,
6732
- fileHash: '00'.repeat(32),
6733
- },
6734
- ],
6735
- },
6736
6249
  });
6737
- method.init();
6738
6250
 
6739
- expect(() => (method as any).prepareExplicitProtocolV2ResourceFiles()).toThrow(
6740
- 'SHA-256 mismatch'
6741
- );
6251
+ await expect((method as any).readProtocolV2DeviceFileHeader('bundle.okpp')).resolves.toBeNull();
6252
+ expect(readChunkLengths.length).toBeGreaterThan(1);
6253
+ expect(Math.max(...readChunkLengths)).toBe(900);
6742
6254
  });
6743
6255
  });
6744
6256
 
@@ -6865,245 +6377,326 @@ describe('Protocol V2 protected method execution', () => {
6865
6377
  });
6866
6378
  });
6867
6379
 
6868
- test.each([
6869
- ['deviceInfoGet', DeviceInfoGet],
6870
- ['deviceStatusGet', DeviceStatusGet],
6871
- ])('does not auto-unlock the read-only %s method', (_name, MethodClass) => {
6872
- const method = new MethodClass({
6873
- id: 1,
6874
- payload: { method: _name },
6875
- } as any);
6876
- method.init();
6380
+ test.each([
6381
+ ['deviceInfoGet', DeviceInfoGet],
6382
+ ['deviceStatusGet', DeviceStatusGet],
6383
+ ])('does not auto-unlock the read-only %s method', (_name, MethodClass) => {
6384
+ const method = new MethodClass({
6385
+ id: 1,
6386
+ payload: { method: _name },
6387
+ } as any);
6388
+ method.init();
6389
+
6390
+ expect(method.unlockPolicy).toBe('none');
6391
+ });
6392
+
6393
+ test('unlocks and retries an opted-in Protocol V2 method once', async () => {
6394
+ const calls: string[] = [];
6395
+ const method = {
6396
+ name: 'testBusinessMethod',
6397
+ unlockPolicy: 'retry-on-locked',
6398
+ protocolV2UiInteraction: { reason: 'settings-page' },
6399
+ run: jest
6400
+ .fn()
6401
+ .mockImplementationOnce(() => {
6402
+ calls.push('run-1');
6403
+ return Promise.reject(deviceLockedError());
6404
+ })
6405
+ .mockImplementationOnce(() => {
6406
+ calls.push('run-2');
6407
+ return Promise.resolve({ message: 'ok' });
6408
+ }),
6409
+ };
6410
+ const device = {
6411
+ isProtocolV2: () => true,
6412
+ unlockDevice: jest.fn(() => {
6413
+ calls.push('unlock');
6414
+ return Promise.resolve();
6415
+ }),
6416
+ };
6417
+ const uiCoordinator = {
6418
+ enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
6419
+ enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
6420
+ resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
6421
+ };
6422
+
6423
+ await expect(
6424
+ runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
6425
+ ).resolves.toEqual({ message: 'ok' });
6426
+ expect(calls).toEqual([
6427
+ 'method-prompt',
6428
+ 'run-1',
6429
+ 'unlock-prompt',
6430
+ 'unlock',
6431
+ 'method-prompt',
6432
+ 'run-2',
6433
+ ]);
6434
+ });
6435
+
6436
+ test('restores the expected hidden-wallet session before retrying after unlock', async () => {
6437
+ const calls: string[] = [];
6438
+ const method = {
6439
+ name: 'evmSignMessage',
6440
+ payload: { passphraseState: 'hidden-state' },
6441
+ useDevicePassphraseState: true,
6442
+ unlockPolicy: 'retry-on-locked',
6443
+ run: jest
6444
+ .fn()
6445
+ .mockImplementationOnce(() => {
6446
+ calls.push('run-1');
6447
+ return Promise.reject(deviceLockedError());
6448
+ })
6449
+ .mockImplementationOnce(() => {
6450
+ calls.push('run-2');
6451
+ return Promise.resolve({ message: 'ok' });
6452
+ }),
6453
+ };
6454
+ const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
6455
+ if (requestType === 'ProtocolInfoRequest') {
6456
+ calls.push('negotiate-session');
6457
+ return Promise.resolve({ message: { version: 2 } });
6458
+ }
6459
+ if (requestType === 'DeviceSessionGet') {
6460
+ calls.push('resume-hidden-session');
6461
+ expect(request).toEqual({ session_id: 'hidden-session' });
6462
+ return Promise.resolve({
6463
+ message: {
6464
+ session_id: 'hidden-session',
6465
+ btc_test_address: 'hidden-state',
6466
+ },
6467
+ });
6468
+ }
6469
+ throw new Error(`Unexpected request: ${requestType}`);
6470
+ });
6471
+ const device = {
6472
+ features: { unlocked: true, passphraseProtection: true },
6473
+ passphraseState: 'hidden-state',
6474
+ commands: { typedCall },
6475
+ isProtocolV2: () => true,
6476
+ unlockDevice: jest.fn(() => {
6477
+ calls.push('unlock');
6478
+ return Promise.resolve();
6479
+ }),
6480
+ getCurrentPassphraseProtection: () => true,
6481
+ getInternalState: () => 'hidden-session',
6482
+ clearInternalState: jest.fn(),
6483
+ getCurrentDeviceId: () => 'wallet-device-id',
6484
+ updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
6485
+ };
6486
+
6487
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
6488
+ message: 'ok',
6489
+ });
6490
+ expect(calls).toEqual([
6491
+ 'run-1',
6492
+ 'unlock',
6493
+ 'negotiate-session',
6494
+ 'resume-hidden-session',
6495
+ 'validate-hidden-session',
6496
+ 'run-2',
6497
+ ]);
6498
+ });
6499
+
6500
+ test('does not replay a business method when the hidden-wallet session cannot be restored after unlock', async () => {
6501
+ const initialError = deviceLockedError();
6502
+ const restoreError = new Error('Invalid wallet session');
6503
+ const method = {
6504
+ name: 'evmSignMessage',
6505
+ payload: { passphraseState: 'hidden-state' },
6506
+ useDevicePassphraseState: true,
6507
+ unlockPolicy: 'retry-on-locked',
6508
+ run: jest.fn().mockRejectedValueOnce(initialError),
6509
+ };
6510
+ const typedCall = jest.fn((requestType: string) => {
6511
+ if (requestType === 'ProtocolInfoRequest') {
6512
+ return Promise.resolve({ message: { version: 2 } });
6513
+ }
6514
+ if (requestType === 'DeviceSessionGet') {
6515
+ return Promise.reject(restoreError);
6516
+ }
6517
+ throw new Error(`Unexpected request: ${requestType}`);
6518
+ });
6519
+ const device = {
6520
+ features: { unlocked: true, passphraseProtection: true },
6521
+ passphraseState: 'hidden-state',
6522
+ commands: { typedCall },
6523
+ isProtocolV2: () => true,
6524
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
6525
+ getCurrentPassphraseProtection: () => true,
6526
+ getInternalState: () => 'hidden-session',
6527
+ clearInternalState: jest.fn(),
6528
+ getCurrentDeviceId: () => 'wallet-device-id',
6529
+ updateInternalState: jest.fn(),
6530
+ };
6877
6531
 
6878
- expect(method.unlockPolicy).toBe('none');
6532
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(restoreError);
6533
+ expect(method.run).toHaveBeenCalledTimes(1);
6534
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
6535
+ expect(device.clearInternalState).toHaveBeenCalledTimes(1);
6536
+ expect(device.updateInternalState).not.toHaveBeenCalled();
6879
6537
  });
6880
6538
 
6881
- test('checks fresh status for a wallet business method without a name allowlist', async () => {
6539
+ test('restores the expected hidden-wallet session after pre-unlock without selecting Attach PIN', async () => {
6882
6540
  const calls: string[] = [];
6883
- const features = { unlocked: true };
6884
6541
  const method = {
6885
- name: 'confluxSignMessageCIP23',
6886
- unlockPolicy: 'none',
6542
+ name: 'evmSignMessage',
6543
+ payload: { passphraseState: 'hidden-state' },
6887
6544
  useDevicePassphraseState: true,
6888
- protocolV2UiInteraction: { reason: 'address-confirmation' },
6545
+ unlockPolicy: 'retry-on-locked',
6889
6546
  run: jest.fn(() => {
6890
6547
  calls.push('run');
6891
- return Promise.resolve({ address: '0x1' });
6548
+ return Promise.resolve({ message: 'ok' });
6892
6549
  }),
6893
6550
  };
6551
+ const typedCall = jest.fn((requestType: string, _responseType: string, request: any) => {
6552
+ if (requestType === 'ProtocolInfoRequest') {
6553
+ calls.push('negotiate-session');
6554
+ return Promise.resolve({ message: { version: 2 } });
6555
+ }
6556
+ if (requestType === 'DeviceSessionGet') {
6557
+ calls.push('resume-hidden-session');
6558
+ expect(request).toEqual({ session_id: 'hidden-session' });
6559
+ return Promise.resolve({
6560
+ message: {
6561
+ session_id: 'hidden-session',
6562
+ btc_test_address: 'hidden-state',
6563
+ },
6564
+ });
6565
+ }
6566
+ throw new Error(`Unexpected request: ${requestType}`);
6567
+ });
6568
+ const features = {
6569
+ unlocked: false,
6570
+ unlockedAttachPin: true,
6571
+ passphraseProtection: true,
6572
+ };
6894
6573
  const device = {
6895
6574
  features,
6896
- commands: {
6897
- typedCall: jest.fn(() => {
6898
- calls.push('status');
6899
- return Promise.resolve({ message: { unlocked: true } });
6900
- }),
6901
- },
6575
+ passphraseState: 'hidden-state',
6576
+ commands: { typedCall },
6902
6577
  isProtocolV2: () => true,
6903
- isBootloader: () => false,
6904
- isRomloader: () => false,
6905
- updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
6906
- features.unlocked = status.unlocked === true;
6907
- return features;
6578
+ unlockDevice: jest.fn(() => {
6579
+ calls.push('unlock-main');
6580
+ features.unlocked = true;
6581
+ features.unlockedAttachPin = false;
6582
+ return Promise.resolve();
6908
6583
  }),
6909
- unlockDevice: jest.fn(),
6910
- };
6911
- const uiCoordinator = {
6912
- enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
6913
- enterUnlockInteraction: jest.fn(),
6584
+ getCurrentPassphraseProtection: () => true,
6585
+ getInternalState: () => 'hidden-session',
6586
+ clearInternalState: jest.fn(),
6587
+ getCurrentDeviceId: () => 'wallet-device-id',
6588
+ updateInternalState: jest.fn(() => calls.push('validate-hidden-session')),
6914
6589
  };
6915
6590
 
6916
- await expect(
6917
- runMethodWithUnlockPolicy(method as any, device as any, {
6918
- uiCoordinator: uiCoordinator as any,
6919
- })
6920
- ).resolves.toEqual({ address: '0x1' });
6921
-
6922
- expect(calls).toEqual(['status', 'method-prompt', 'run']);
6923
- expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
6924
- expect(device.unlockDevice).not.toHaveBeenCalled();
6925
- expect(method.run).toHaveBeenCalledTimes(1);
6591
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
6592
+ message: 'ok',
6593
+ });
6594
+ expect(device.unlockDevice).toHaveBeenCalledWith();
6595
+ expect(calls).toEqual([
6596
+ 'unlock-main',
6597
+ 'negotiate-session',
6598
+ 'resume-hidden-session',
6599
+ 'validate-hidden-session',
6600
+ 'run',
6601
+ ]);
6926
6602
  });
6927
6603
 
6928
- test('validates the fresh device identity before starting unlock', async () => {
6929
- const calls: string[] = [];
6930
- const identityError = new Error('Unexpected device');
6604
+ test('does not restore a hidden-wallet session for a standard-wallet pre-unlock', async () => {
6931
6605
  const method = {
6932
- name: 'evmSignMessage',
6933
- unlockPolicy: 'none',
6606
+ name: 'evmGetAddress',
6607
+ payload: { useEmptyPassphrase: true },
6934
6608
  useDevicePassphraseState: true,
6935
- run: jest.fn(),
6609
+ unlockPolicy: 'retry-on-locked',
6610
+ run: jest.fn().mockResolvedValue({ address: 'standard-wallet-address' }),
6936
6611
  };
6937
- const features = { unlocked: false };
6938
6612
  const device = {
6939
- features,
6940
- commands: {
6941
- typedCall: jest.fn(() => {
6942
- calls.push('status');
6943
- return Promise.resolve({ message: { unlocked: false } });
6944
- }),
6945
- },
6613
+ features: { unlocked: false },
6614
+ passphraseState: 'stale-hidden-state',
6946
6615
  isProtocolV2: () => true,
6947
- isBootloader: () => false,
6948
- isRomloader: () => false,
6949
- updateProtocolV2Status: jest.fn(() => features),
6950
- unlockDevice: jest.fn(),
6616
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
6951
6617
  };
6952
6618
 
6953
- await expect(
6954
- runMethodWithUnlockPolicy(method as any, device as any, {
6955
- afterStatusBeforeUnlock: () => {
6956
- calls.push('identity');
6957
- throw identityError;
6958
- },
6959
- })
6960
- ).rejects.toBe(identityError);
6961
-
6962
- expect(calls).toEqual(['status', 'identity']);
6963
- expect(device.unlockDevice).not.toHaveBeenCalled();
6964
- expect(method.run).not.toHaveBeenCalled();
6619
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
6620
+ address: 'standard-wallet-address',
6621
+ });
6622
+ expect(device.unlockDevice).toHaveBeenCalledTimes(1);
6623
+ expect(method.run).toHaveBeenCalledTimes(1);
6965
6624
  });
6966
6625
 
6967
- test('unlocks before business and never replays the callback', async () => {
6626
+ test('refreshes status and unlocks before a protected Protocol V2 method', async () => {
6968
6627
  const calls: string[] = [];
6969
- const features = { unlocked: true };
6970
6628
  const method = {
6971
- name: 'evmSignMessage',
6629
+ name: 'deviceSettingsPageShow',
6972
6630
  unlockPolicy: 'unlock-before-run',
6973
- protocolV2UiInteraction: { reason: 'signing-confirmation' },
6631
+ protocolV2UiInteraction: { reason: 'settings-page' },
6974
6632
  run: jest.fn(() => {
6975
6633
  calls.push('run');
6976
- return Promise.resolve({ signature: 'signed' });
6634
+ return Promise.resolve({ message: 'ok' });
6977
6635
  }),
6978
6636
  };
6637
+ const features = { unlocked: true };
6979
6638
  const device = {
6980
6639
  features,
6981
6640
  commands: {
6982
6641
  typedCall: jest.fn(() => {
6983
- calls.push('status');
6642
+ calls.push('refresh-status');
6984
6643
  return Promise.resolve({ message: { unlocked: false } });
6985
6644
  }),
6986
6645
  },
6987
6646
  isProtocolV2: () => true,
6988
- isBootloader: () => false,
6989
- isRomloader: () => false,
6990
6647
  updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
6991
- features.unlocked = status.unlocked === true;
6648
+ features.unlocked = status.unlocked ?? features.unlocked;
6992
6649
  return features;
6993
6650
  }),
6994
6651
  unlockDevice: jest.fn(() => {
6995
6652
  calls.push('unlock');
6996
6653
  features.unlocked = true;
6997
- return Promise.resolve(features);
6654
+ return Promise.resolve();
6998
6655
  }),
6999
6656
  };
7000
6657
  const uiCoordinator = {
7001
6658
  enterMethodInteraction: jest.fn(() => calls.push('method-prompt')),
7002
- enterUnlockInteraction: jest.fn(() => {
7003
- calls.push('unlock-prompt');
7004
- return undefined;
7005
- }),
6659
+ enterUnlockInteraction: jest.fn(() => calls.push('unlock-prompt')),
6660
+ resumeMethodInteraction: jest.fn(() => calls.push('method-prompt')),
7006
6661
  };
7007
6662
 
7008
6663
  await expect(
7009
- runMethodWithUnlockPolicy(method as any, device as any, {
7010
- uiCoordinator: uiCoordinator as any,
7011
- prepare: () => {
7012
- calls.push('wallet-session');
7013
- return Promise.resolve();
7014
- },
7015
- })
7016
- ).resolves.toEqual({ signature: 'signed' });
7017
-
7018
- expect(calls).toEqual([
7019
- 'status',
7020
- 'unlock-prompt',
7021
- 'unlock',
7022
- 'wallet-session',
7023
- 'method-prompt',
7024
- 'run',
7025
- ]);
7026
- expect(device.unlockDevice).toHaveBeenCalledTimes(1);
6664
+ runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
6665
+ ).resolves.toEqual({ message: 'ok' });
6666
+ expect(calls).toEqual(['refresh-status', 'unlock-prompt', 'unlock', 'method-prompt', 'run']);
6667
+ expect(device.commands.typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {});
7027
6668
  expect(method.run).toHaveBeenCalledTimes(1);
6669
+ expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledTimes(1);
6670
+ expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
7028
6671
  });
7029
6672
 
7030
- test('fails closed when fresh status does not explicitly report unlocked state', async () => {
7031
- const method = {
7032
- name: 'evmGetAddress',
7033
- unlockPolicy: 'unlock-before-run',
7034
- run: jest.fn(),
7035
- };
7036
- const device = {
7037
- features: { unlocked: undefined },
7038
- commands: { typedCall: jest.fn().mockResolvedValue({ message: {} }) },
7039
- isProtocolV2: () => true,
7040
- isBootloader: () => false,
7041
- isRomloader: () => false,
7042
- updateProtocolV2Status: jest.fn(() => ({ unlocked: undefined })),
7043
- unlockDevice: jest.fn(),
7044
- };
7045
-
7046
- await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toMatchObject({
7047
- errorCode: HardwareErrorCode.RuntimeError,
7048
- });
7049
- expect(device.unlockDevice).not.toHaveBeenCalled();
7050
- expect(method.run).not.toHaveBeenCalled();
7051
- });
7052
-
7053
- test('returns a business DeviceLocked error without unlocking or replaying', async () => {
7054
- const error = deviceLockedError();
6673
+ test('uses refreshed unlocked status instead of a stale locked cache', async () => {
6674
+ const features = { unlocked: false };
7055
6675
  const method = {
7056
- name: 'evmSignMessage',
6676
+ name: 'deviceSettings',
7057
6677
  unlockPolicy: 'unlock-before-run',
7058
- run: jest.fn().mockRejectedValue(error),
6678
+ run: jest.fn().mockResolvedValue({ message: 'ok' }),
7059
6679
  };
7060
- const features = { unlocked: true };
7061
6680
  const device = {
7062
6681
  features,
7063
- commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
6682
+ commands: {
6683
+ typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }),
6684
+ },
7064
6685
  isProtocolV2: () => true,
7065
- isBootloader: () => false,
7066
- isRomloader: () => false,
7067
- updateProtocolV2Status: jest.fn(() => features),
6686
+ updateProtocolV2Status: jest.fn((status: { unlocked?: boolean }) => {
6687
+ features.unlocked = status.unlocked ?? features.unlocked;
6688
+ return features;
6689
+ }),
7068
6690
  unlockDevice: jest.fn(),
7069
6691
  };
7070
6692
 
7071
- await expect(runMethodWithUnlockPolicy(method as any, device as any)).rejects.toBe(error);
6693
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
6694
+ message: 'ok',
6695
+ });
7072
6696
  expect(device.unlockDevice).not.toHaveBeenCalled();
7073
6697
  expect(method.run).toHaveBeenCalledTimes(1);
7074
6698
  });
7075
6699
 
7076
- test('reuses one lightweight preflight context across nested protected methods', async () => {
7077
- const features = { unlocked: true };
7078
- const context = createProtocolV2UnlockContext();
7079
- const firstMethod = {
7080
- name: 'allNetworkGetAddress',
7081
- unlockPolicy: 'unlock-before-run',
7082
- run: jest.fn().mockResolvedValue({ root: true }),
7083
- };
7084
- const nestedMethod = {
7085
- name: 'evmGetAddress',
7086
- unlockPolicy: 'unlock-before-run',
7087
- run: jest.fn().mockResolvedValue({ address: '0x1' }),
7088
- };
7089
- const device = {
7090
- features,
7091
- commands: { typedCall: jest.fn().mockResolvedValue({ message: { unlocked: true } }) },
7092
- isProtocolV2: () => true,
7093
- isBootloader: () => false,
7094
- isRomloader: () => false,
7095
- updateProtocolV2Status: jest.fn(() => features),
7096
- unlockDevice: jest.fn(),
7097
- };
7098
-
7099
- await runMethodWithUnlockPolicy(firstMethod as any, device as any, { context });
7100
- await runMethodWithUnlockPolicy(nestedMethod as any, device as any, { context });
7101
-
7102
- expect(device.commands.typedCall).toHaveBeenCalledTimes(1);
7103
- expect(firstMethod.run).toHaveBeenCalledTimes(1);
7104
- expect(nestedMethod.run).toHaveBeenCalledTimes(1);
7105
- });
7106
-
7107
6700
  test.each(['bootloader', 'romloader'] as const)(
7108
6701
  'skips DeviceStatus and unlock in Protocol V2 %s mode',
7109
6702
  async mode => {
@@ -7121,7 +6714,7 @@ describe('Protocol V2 protected method execution', () => {
7121
6714
  unlockDevice: jest.fn(),
7122
6715
  };
7123
6716
 
7124
- await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
6717
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({
7125
6718
  message: 'updating',
7126
6719
  });
7127
6720
  expect(device.commands.typedCall).not.toHaveBeenCalled();
@@ -7130,30 +6723,157 @@ describe('Protocol V2 protected method execution', () => {
7130
6723
  }
7131
6724
  );
7132
6725
 
7133
- test('keeps Protocol V1 and lock-free methods outside the preflight path', async () => {
6726
+ test('infers a signing interaction before running a Protocol V2 business method', async () => {
6727
+ const method = {
6728
+ name: 'evmSignMessage',
6729
+ params: { message: 'not-exposed-in-event' },
6730
+ payload: {},
6731
+ unlockPolicy: 'retry-on-locked',
6732
+ run: jest.fn().mockResolvedValue({ signature: 'test' }),
6733
+ };
6734
+ const device = {
6735
+ isProtocolV2: () => true,
6736
+ unlockDevice: jest.fn(),
6737
+ };
6738
+ const uiCoordinator = {
6739
+ enterMethodInteraction: jest.fn(),
6740
+ enterUnlockInteraction: jest.fn(),
6741
+ resumeMethodInteraction: jest.fn(),
6742
+ };
6743
+
6744
+ await runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any);
6745
+
6746
+ expect(uiCoordinator.enterMethodInteraction).toHaveBeenCalledWith({
6747
+ request: 'button',
6748
+ source: 'method-lifecycle',
6749
+ reason: 'signing-confirmation',
6750
+ completion: 'operation-completed',
6751
+ deviceOnly: true,
6752
+ operation: 'evmSignMessage',
6753
+ });
6754
+ });
6755
+
6756
+ test('does not unlock a Protocol V1 device or a method without the policy', async () => {
7134
6757
  for (const [isProtocolV2, unlockPolicy] of [
7135
- [false, 'unlock-before-run'],
6758
+ [false, 'retry-on-locked'],
7136
6759
  [true, 'none'],
7137
6760
  ] as const) {
6761
+ const error = deviceLockedError();
7138
6762
  const method = {
7139
6763
  unlockPolicy,
7140
- useDevicePassphraseState: false,
7141
- run: jest.fn().mockResolvedValue({ message: 'ok' }),
6764
+ run: jest.fn().mockRejectedValue(error),
7142
6765
  };
7143
6766
  const device = {
7144
- commands: { typedCall: jest.fn() },
7145
6767
  isProtocolV2: () => isProtocolV2,
7146
6768
  unlockDevice: jest.fn(),
7147
6769
  };
7148
6770
 
7149
- await expect(runMethodWithUnlockPolicy(method as any, device as any)).resolves.toEqual({
7150
- message: 'ok',
7151
- });
7152
- expect(device.commands.typedCall).not.toHaveBeenCalled();
6771
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
7153
6772
  expect(device.unlockDevice).not.toHaveBeenCalled();
7154
6773
  expect(method.run).toHaveBeenCalledTimes(1);
7155
6774
  }
7156
6775
  });
6776
+
6777
+ test('runs lock-free eventless methods without unlocking or synthesized UI', async () => {
6778
+ const method = {
6779
+ name: 'uploadPortfolio',
6780
+ unlockPolicy: 'none',
6781
+ protocolV2UiMode: 'none',
6782
+ run: jest.fn().mockResolvedValue({ message: 'ok' }),
6783
+ };
6784
+ const device = {
6785
+ features: { unlocked: false },
6786
+ isProtocolV2: () => true,
6787
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
6788
+ };
6789
+ const uiCoordinator = {
6790
+ enterMethodInteraction: jest.fn(),
6791
+ enterUnlockInteraction: jest.fn(),
6792
+ resumeMethodInteraction: jest.fn(),
6793
+ };
6794
+
6795
+ await expect(
6796
+ runMethodWithUnlockRetry(method as any, device as any, uiCoordinator as any)
6797
+ ).resolves.toEqual({ message: 'ok' });
6798
+ expect(device.unlockDevice).not.toHaveBeenCalled();
6799
+ expect(uiCoordinator.enterMethodInteraction).not.toHaveBeenCalled();
6800
+ expect(uiCoordinator.enterUnlockInteraction).not.toHaveBeenCalled();
6801
+ expect(uiCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
6802
+ });
6803
+
6804
+ test('does not unlock or replay a lock-free state-changing method after a locked response', async () => {
6805
+ const error = deviceLockedError();
6806
+ const method = {
6807
+ name: 'uploadPortfolio',
6808
+ unlockPolicy: 'none',
6809
+ run: jest.fn().mockRejectedValue(error),
6810
+ };
6811
+ const device = {
6812
+ features: { unlocked: true },
6813
+ isProtocolV2: () => true,
6814
+ unlockDevice: jest.fn(),
6815
+ };
6816
+
6817
+ await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error);
6818
+ expect(method.run).toHaveBeenCalledTimes(1);
6819
+ expect(device.unlockDevice).not.toHaveBeenCalled();
6820
+ });
6821
+
6822
+ test('does not retry when unlock fails or when the retry is still locked', async () => {
6823
+ const initialError = deviceLockedError();
6824
+ const unlockError = new Error('PIN cancelled');
6825
+ const unlockFailMethod = {
6826
+ unlockPolicy: 'retry-on-locked',
6827
+ run: jest.fn().mockRejectedValue(initialError),
6828
+ };
6829
+ const unlockFailDevice = {
6830
+ isProtocolV2: () => true,
6831
+ unlockDevice: jest.fn().mockRejectedValue(unlockError),
6832
+ };
6833
+ const unlockFailCoordinator = {
6834
+ enterMethodInteraction: jest.fn(),
6835
+ enterUnlockInteraction: jest.fn(),
6836
+ resumeMethodInteraction: jest.fn(),
6837
+ };
6838
+
6839
+ await expect(
6840
+ runMethodWithUnlockRetry(
6841
+ unlockFailMethod as any,
6842
+ unlockFailDevice as any,
6843
+ unlockFailCoordinator as any
6844
+ )
6845
+ ).rejects.toBe(unlockError);
6846
+ expect(unlockFailMethod.run).toHaveBeenCalledTimes(1);
6847
+ expect(unlockFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
6848
+ expect(unlockFailCoordinator.resumeMethodInteraction).not.toHaveBeenCalled();
6849
+
6850
+ const retryError = deviceLockedError();
6851
+ const retryFailMethod = {
6852
+ unlockPolicy: 'retry-on-locked',
6853
+ run: jest.fn().mockRejectedValueOnce(initialError).mockRejectedValueOnce(retryError),
6854
+ };
6855
+ const retryFailDevice = {
6856
+ isProtocolV2: () => true,
6857
+ unlockDevice: jest.fn().mockResolvedValue(undefined),
6858
+ };
6859
+ const retryFailCoordinator = {
6860
+ enterMethodInteraction: jest.fn(),
6861
+ enterUnlockInteraction: jest.fn(),
6862
+ resumeMethodInteraction: jest.fn(),
6863
+ };
6864
+
6865
+ await expect(
6866
+ runMethodWithUnlockRetry(
6867
+ retryFailMethod as any,
6868
+ retryFailDevice as any,
6869
+ retryFailCoordinator as any
6870
+ )
6871
+ ).rejects.toBe(retryError);
6872
+ expect(retryFailMethod.run).toHaveBeenCalledTimes(2);
6873
+ expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1);
6874
+ expect(retryFailCoordinator.enterUnlockInteraction).toHaveBeenCalledTimes(1);
6875
+ expect(retryFailCoordinator.resumeMethodInteraction).toHaveBeenCalledTimes(1);
6876
+ });
7157
6877
  });
7158
6878
 
7159
6879
  describe('Protocol V2 current low-level methods', () => {
@@ -7285,9 +7005,7 @@ describe('Protocol V2 current low-level methods', () => {
7285
7005
 
7286
7006
  await method.run();
7287
7007
 
7288
- expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {
7289
- eventless_wallet_session: true,
7290
- });
7008
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {});
7291
7009
  });
7292
7010
 
7293
7011
  test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
@@ -7363,29 +7081,6 @@ describe('Protocol V2 current low-level methods', () => {
7363
7081
 
7364
7082
  await expect(method.run()).resolves.toBe(features);
7365
7083
  expect(unlockDevice).toHaveBeenCalledTimes(1);
7366
- expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Main, {
7367
- emitUiEvent: false,
7368
- });
7369
- });
7370
-
7371
- test('forwards an explicit PIN type when unlocking a Protocol V2 device', async () => {
7372
- const method = new DeviceUnlock({
7373
- id: 1,
7374
- payload: {
7375
- method: 'deviceUnlock',
7376
- pinType: DeviceSessionPinType.Any,
7377
- },
7378
- });
7379
- method.init();
7380
-
7381
- const unlockDevice = jest.fn().mockResolvedValue({ unlocked: true });
7382
- (method as any).device = { unlockDevice };
7383
-
7384
- await method.run();
7385
-
7386
- expect(unlockDevice).toHaveBeenCalledWith(DeviceSessionPinType.Any, {
7387
- emitUiEvent: false,
7388
- });
7389
7084
  });
7390
7085
 
7391
7086
  test.each([