@onekeyfe/hd-core 1.1.27-patch.1 → 1.2.0-alpha.0

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 (377) hide show
  1. package/__tests__/evmLedgerLegacySafety.test.ts +15 -4
  2. package/__tests__/evmSignTransaction.test.ts +1 -1
  3. package/__tests__/evmSignTypedData.test.ts +1 -1
  4. package/__tests__/logBlockEvent.test.ts +37 -0
  5. package/__tests__/protocol-v2.test.ts +3025 -0
  6. package/dist/api/BaseMethod.d.ts +1 -0
  7. package/dist/api/BaseMethod.d.ts.map +1 -1
  8. package/dist/api/DirList.d.ts +10 -0
  9. package/dist/api/DirList.d.ts.map +1 -0
  10. package/dist/api/DirMake.d.ts +9 -0
  11. package/dist/api/DirMake.d.ts.map +1 -0
  12. package/dist/api/DirRemove.d.ts +9 -0
  13. package/dist/api/DirRemove.d.ts.map +1 -0
  14. package/dist/api/FileDelete.d.ts +9 -0
  15. package/dist/api/FileDelete.d.ts.map +1 -0
  16. package/dist/api/FileRead.d.ts +19 -0
  17. package/dist/api/FileRead.d.ts.map +1 -0
  18. package/dist/api/FileWrite.d.ts +24 -0
  19. package/dist/api/FileWrite.d.ts.map +1 -0
  20. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  22. package/dist/api/FirmwareUpdateV3.d.ts +1 -0
  23. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  24. package/dist/api/FirmwareUpdateV4.d.ts +35 -0
  25. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -0
  26. package/dist/api/GetDeviceInfo.d.ts +9 -0
  27. package/dist/api/GetDeviceInfo.d.ts.map +1 -0
  28. package/dist/api/GetFeatures.d.ts.map +1 -1
  29. package/dist/api/GetOnekeyFeatures.d.ts.map +1 -1
  30. package/dist/api/GetPassphraseState.d.ts +6 -1
  31. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  32. package/dist/api/PathInfo.d.ts +10 -0
  33. package/dist/api/PathInfo.d.ts.map +1 -0
  34. package/dist/api/PromptWebDeviceAccess.d.ts.map +1 -1
  35. package/dist/api/SearchDevices.d.ts +2 -1
  36. package/dist/api/SearchDevices.d.ts.map +1 -1
  37. package/dist/api/alephium/AlephiumGetAddress.d.ts +2 -6
  38. package/dist/api/alephium/AlephiumGetAddress.d.ts.map +1 -1
  39. package/dist/api/alephium/AlephiumSignMessage.d.ts +2 -5
  40. package/dist/api/alephium/AlephiumSignMessage.d.ts.map +1 -1
  41. package/dist/api/alephium/AlephiumSignTransaction.d.ts +5 -8
  42. package/dist/api/alephium/AlephiumSignTransaction.d.ts.map +1 -1
  43. package/dist/api/aptos/AptosGetAddress.d.ts.map +1 -1
  44. package/dist/api/benfen/BenfenGetAddress.d.ts +2 -9
  45. package/dist/api/benfen/BenfenGetAddress.d.ts.map +1 -1
  46. package/dist/api/benfen/BenfenGetPublicKey.d.ts +2 -9
  47. package/dist/api/benfen/BenfenGetPublicKey.d.ts.map +1 -1
  48. package/dist/api/benfen/BenfenSignMessage.d.ts +2 -8
  49. package/dist/api/benfen/BenfenSignMessage.d.ts.map +1 -1
  50. package/dist/api/benfen/BenfenSignTransaction.d.ts +2 -8
  51. package/dist/api/benfen/BenfenSignTransaction.d.ts.map +1 -1
  52. package/dist/api/btc/BTCGetAddress.d.ts +1 -11
  53. package/dist/api/btc/BTCGetAddress.d.ts.map +1 -1
  54. package/dist/api/btc/BTCGetPublicKey.d.ts +1 -11
  55. package/dist/api/btc/BTCGetPublicKey.d.ts.map +1 -1
  56. package/dist/api/btc/BTCSignMessage.d.ts +1 -15
  57. package/dist/api/btc/BTCSignMessage.d.ts.map +1 -1
  58. package/dist/api/btc/BTCSignPsbt.d.ts.map +1 -1
  59. package/dist/api/btc/BTCSignTransaction.d.ts +1 -11
  60. package/dist/api/btc/BTCSignTransaction.d.ts.map +1 -1
  61. package/dist/api/btc/BTCVerifyMessage.d.ts +1 -11
  62. package/dist/api/btc/BTCVerifyMessage.d.ts.map +1 -1
  63. package/dist/api/btc/helpers/versionLimit.d.ts +2 -11
  64. package/dist/api/btc/helpers/versionLimit.d.ts.map +1 -1
  65. package/dist/api/cardano/CardanoSignTransaction.d.ts.map +1 -1
  66. package/dist/api/conflux/ConfluxSignTransaction.d.ts.map +1 -1
  67. package/dist/api/device/DeviceLock.d.ts.map +1 -1
  68. package/dist/api/device/DeviceRebootToBoardloader.d.ts +1 -1
  69. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  70. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  71. package/dist/api/device/DeviceSupportFeatures.d.ts.map +1 -1
  72. package/dist/api/device/DeviceUploadResource.d.ts.map +1 -1
  73. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  74. package/dist/api/dynex/DnxGetAddress.d.ts.map +1 -1
  75. package/dist/api/dynex/DnxSignTransaction.d.ts.map +1 -1
  76. package/dist/api/evm/EVMGetPublicKey.d.ts.map +1 -1
  77. package/dist/api/evm/EVMSignMessageEIP712.d.ts +2 -8
  78. package/dist/api/evm/EVMSignMessageEIP712.d.ts.map +1 -1
  79. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  80. package/dist/api/evm/latest/signTypedData.d.ts +1 -1
  81. package/dist/api/evm/latest/signTypedData.d.ts.map +1 -1
  82. package/dist/api/evm/latest/signTypedHash.d.ts.map +1 -1
  83. package/dist/api/evm/legacyV1/getAddress.d.ts.map +1 -1
  84. package/dist/api/evm/legacyV1/getPublicKey.d.ts.map +1 -1
  85. package/dist/api/evm/legacyV1/signMessage.d.ts.map +1 -1
  86. package/dist/api/evm/legacyV1/signTypedData.d.ts +1 -1
  87. package/dist/api/evm/legacyV1/signTypedData.d.ts.map +1 -1
  88. package/dist/api/evm/legacyV1/signTypedHash.d.ts.map +1 -1
  89. package/dist/api/evm/legacyV1/verifyMessage.d.ts.map +1 -1
  90. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  91. package/dist/api/helpers/batchGetPublickeys.d.ts +3 -0
  92. package/dist/api/helpers/batchGetPublickeys.d.ts.map +1 -1
  93. package/dist/api/helpers/filesystemValidation.d.ts +7 -0
  94. package/dist/api/helpers/filesystemValidation.d.ts.map +1 -0
  95. package/dist/api/index.d.ts +32 -0
  96. package/dist/api/index.d.ts.map +1 -1
  97. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  98. package/dist/api/lightning/LnurlAuth.d.ts +4 -0
  99. package/dist/api/lightning/LnurlAuth.d.ts.map +1 -1
  100. package/dist/api/neo/NeoGetAddress.d.ts +2 -8
  101. package/dist/api/neo/NeoGetAddress.d.ts.map +1 -1
  102. package/dist/api/neo/NeoSignTransaction.d.ts +2 -8
  103. package/dist/api/neo/NeoSignTransaction.d.ts.map +1 -1
  104. package/dist/api/nervos/NervosGetAddress.d.ts +2 -9
  105. package/dist/api/nervos/NervosGetAddress.d.ts.map +1 -1
  106. package/dist/api/nervos/NervosSignTransaction.d.ts +9 -12
  107. package/dist/api/nervos/NervosSignTransaction.d.ts.map +1 -1
  108. package/dist/api/nexa/NexaGetAddress.d.ts +2 -8
  109. package/dist/api/nexa/NexaGetAddress.d.ts.map +1 -1
  110. package/dist/api/nexa/NexaSignTransaction.d.ts +2 -9
  111. package/dist/api/nexa/NexaSignTransaction.d.ts.map +1 -1
  112. package/dist/api/polkadot/PolkadotGetAddress.d.ts +3 -0
  113. package/dist/api/polkadot/PolkadotGetAddress.d.ts.map +1 -1
  114. package/dist/api/polkadot/networks.d.ts +3 -0
  115. package/dist/api/polkadot/networks.d.ts.map +1 -1
  116. package/dist/api/protocol-v2/DevReboot.d.ts +7 -0
  117. package/dist/api/protocol-v2/DevReboot.d.ts.map +1 -0
  118. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +7 -0
  119. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -0
  120. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts +29 -0
  121. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +1 -0
  122. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +6 -0
  123. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -0
  124. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +6 -0
  125. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +1 -0
  126. package/dist/api/protocol-v2/DeviceReboot.d.ts +7 -0
  127. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -0
  128. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +7 -0
  129. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +1 -0
  130. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +6 -0
  131. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +1 -0
  132. package/dist/api/protocol-v2/FilesystemDiskControl.d.ts +13 -0
  133. package/dist/api/protocol-v2/FilesystemDiskControl.d.ts.map +1 -0
  134. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts +6 -0
  135. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +1 -0
  136. package/dist/api/protocol-v2/FilesystemFormat.d.ts +6 -0
  137. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -0
  138. package/dist/api/protocol-v2/GetProtoVersion.d.ts +6 -0
  139. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +1 -0
  140. package/dist/api/protocol-v2/Ping.d.ts +8 -0
  141. package/dist/api/protocol-v2/Ping.d.ts.map +1 -0
  142. package/dist/api/protocol-v2/helpers.d.ts +34 -0
  143. package/dist/api/protocol-v2/helpers.d.ts.map +1 -0
  144. package/dist/api/scdo/ScdoGetAddress.d.ts +2 -6
  145. package/dist/api/scdo/ScdoGetAddress.d.ts.map +1 -1
  146. package/dist/api/scdo/ScdoSignMessage.d.ts +2 -5
  147. package/dist/api/scdo/ScdoSignMessage.d.ts.map +1 -1
  148. package/dist/api/scdo/ScdoSignTransaction.d.ts +2 -5
  149. package/dist/api/scdo/ScdoSignTransaction.d.ts.map +1 -1
  150. package/dist/api/solana/SolGetAddress.d.ts +1 -0
  151. package/dist/api/solana/SolGetAddress.d.ts.map +1 -1
  152. package/dist/api/solana/SolSignMessage.d.ts +3 -0
  153. package/dist/api/solana/SolSignMessage.d.ts.map +1 -1
  154. package/dist/api/solana/SolSignOffchainMessage.d.ts +3 -0
  155. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  156. package/dist/api/solana/SolSignTransaction.d.ts +6 -0
  157. package/dist/api/solana/SolSignTransaction.d.ts.map +1 -1
  158. package/dist/api/stellar/StellarGetAddress.d.ts +2 -1
  159. package/dist/api/stellar/StellarGetAddress.d.ts.map +1 -1
  160. package/dist/api/stellar/StellarSignTransaction.d.ts +3 -2
  161. package/dist/api/stellar/StellarSignTransaction.d.ts.map +1 -1
  162. package/dist/api/sui/SuiGetAddress.d.ts +3 -0
  163. package/dist/api/sui/SuiGetAddress.d.ts.map +1 -1
  164. package/dist/api/sui/SuiGetPublicKey.d.ts +3 -0
  165. package/dist/api/sui/SuiGetPublicKey.d.ts.map +1 -1
  166. package/dist/api/sui/SuiSignMessage.d.ts +3 -0
  167. package/dist/api/sui/SuiSignMessage.d.ts.map +1 -1
  168. package/dist/api/sui/SuiSignTransaction.d.ts +5 -2
  169. package/dist/api/sui/SuiSignTransaction.d.ts.map +1 -1
  170. package/dist/api/ton/TonGetAddress.d.ts +3 -0
  171. package/dist/api/ton/TonGetAddress.d.ts.map +1 -1
  172. package/dist/api/ton/TonSignData.d.ts +5 -0
  173. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  174. package/dist/api/ton/TonSignMessage.d.ts +3 -0
  175. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  176. package/dist/api/ton/TonSignProof.d.ts +3 -0
  177. package/dist/api/ton/TonSignProof.d.ts.map +1 -1
  178. package/dist/api/tron/TronSignMessage.d.ts +4 -0
  179. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  180. package/dist/api/tron/TronSignTransaction.d.ts +4 -0
  181. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  182. package/dist/core/index.d.ts.map +1 -1
  183. package/dist/data-manager/DataManager.d.ts +7 -4
  184. package/dist/data-manager/DataManager.d.ts.map +1 -1
  185. package/dist/data-manager/MessagesConfig.d.ts +2 -2
  186. package/dist/data-manager/MessagesConfig.d.ts.map +1 -1
  187. package/dist/data-manager/TransportManager.d.ts +5 -4
  188. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  189. package/dist/device/Device.d.ts +36 -10
  190. package/dist/device/Device.d.ts.map +1 -1
  191. package/dist/device/DeviceCommands.d.ts +9 -9
  192. package/dist/device/DeviceCommands.d.ts.map +1 -1
  193. package/dist/device/DeviceConnector.d.ts +2 -1
  194. package/dist/device/DeviceConnector.d.ts.map +1 -1
  195. package/dist/deviceProfile/buildDeviceProfile.d.ts +22 -0
  196. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -0
  197. package/dist/deviceProfile/index.d.ts +3 -0
  198. package/dist/deviceProfile/index.d.ts.map +1 -0
  199. package/dist/deviceProfile/legacyFeaturesView.d.ts +5 -0
  200. package/dist/deviceProfile/legacyFeaturesView.d.ts.map +1 -0
  201. package/dist/events/logBlockEvent.d.ts +1 -0
  202. package/dist/events/logBlockEvent.d.ts.map +1 -1
  203. package/dist/events/ui-request.d.ts +8 -0
  204. package/dist/events/ui-request.d.ts.map +1 -1
  205. package/dist/index.d.ts +720 -382
  206. package/dist/index.js +17634 -1201
  207. package/dist/inject.d.ts.map +1 -1
  208. package/dist/protocols/protocol-v2/features.d.ts +93 -0
  209. package/dist/protocols/protocol-v2/features.d.ts.map +1 -0
  210. package/dist/protocols/protocol-v2/firmware.d.ts +13 -0
  211. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -0
  212. package/dist/protocols/protocol-v2/index.d.ts +4 -0
  213. package/dist/protocols/protocol-v2/index.d.ts.map +1 -0
  214. package/dist/types/api/export.d.ts +1 -1
  215. package/dist/types/api/export.d.ts.map +1 -1
  216. package/dist/types/api/firmwareUpdate.d.ts +27 -0
  217. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  218. package/dist/types/api/getDeviceInfo.d.ts +85 -0
  219. package/dist/types/api/getDeviceInfo.d.ts.map +1 -0
  220. package/dist/types/api/getPassphraseState.d.ts +10 -1
  221. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  222. package/dist/types/api/index.d.ts +37 -1
  223. package/dist/types/api/index.d.ts.map +1 -1
  224. package/dist/types/api/protocolV2.d.ts +103 -0
  225. package/dist/types/api/protocolV2.d.ts.map +1 -0
  226. package/dist/types/api/searchDevices.d.ts +2 -2
  227. package/dist/types/api/searchDevices.d.ts.map +1 -1
  228. package/dist/types/device.d.ts +10 -3
  229. package/dist/types/device.d.ts.map +1 -1
  230. package/dist/types/params.d.ts +2 -0
  231. package/dist/types/params.d.ts.map +1 -1
  232. package/dist/types/settings.d.ts +1 -1
  233. package/dist/types/settings.d.ts.map +1 -1
  234. package/dist/utils/deviceFeaturesUtils.d.ts +6 -8
  235. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  236. package/dist/utils/deviceInfoUtils.d.ts +1 -0
  237. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  238. package/dist/utils/index.d.ts +1 -1
  239. package/dist/utils/index.d.ts.map +1 -1
  240. package/dist/utils/patch.d.ts +1 -1
  241. package/dist/utils/patch.d.ts.map +1 -1
  242. package/package.json +4 -4
  243. package/src/api/BaseMethod.ts +26 -22
  244. package/src/api/DirList.ts +31 -0
  245. package/src/api/DirMake.ts +23 -0
  246. package/src/api/DirRemove.ts +23 -0
  247. package/src/api/FileDelete.ts +23 -0
  248. package/src/api/FileRead.ts +167 -0
  249. package/src/api/FileWrite.ts +216 -0
  250. package/src/api/FirmwareUpdate.ts +13 -5
  251. package/src/api/FirmwareUpdateV2.ts +21 -25
  252. package/src/api/FirmwareUpdateV3.ts +17 -4
  253. package/src/api/FirmwareUpdateV4.ts +827 -0
  254. package/src/api/GetDeviceInfo.ts +152 -0
  255. package/src/api/GetFeatures.ts +5 -2
  256. package/src/api/GetOnekeyFeatures.ts +95 -3
  257. package/src/api/GetPassphraseState.ts +16 -10
  258. package/src/api/PathInfo.ts +39 -0
  259. package/src/api/PromptWebDeviceAccess.ts +11 -1
  260. package/src/api/SearchDevices.ts +7 -2
  261. package/src/api/alephium/AlephiumGetAddress.ts +6 -2
  262. package/src/api/alephium/AlephiumSignMessage.ts +6 -1
  263. package/src/api/alephium/AlephiumSignTransaction.ts +15 -4
  264. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +21 -13
  265. package/src/api/aptos/AptosGetAddress.ts +2 -3
  266. package/src/api/benfen/BenfenGetAddress.ts +11 -7
  267. package/src/api/benfen/BenfenGetPublicKey.ts +6 -2
  268. package/src/api/benfen/BenfenSignMessage.ts +6 -1
  269. package/src/api/benfen/BenfenSignTransaction.ts +6 -1
  270. package/src/api/btc/BTCGetPublicKey.ts +3 -2
  271. package/src/api/btc/BTCSignPsbt.ts +1 -2
  272. package/src/api/btc/helpers/versionLimit.ts +7 -1
  273. package/src/api/cardano/CardanoSignTransaction.ts +2 -4
  274. package/src/api/conflux/ConfluxSignTransaction.ts +5 -2
  275. package/src/api/device/DeviceFullyUploadResource.ts +3 -3
  276. package/src/api/device/DeviceLock.ts +1 -3
  277. package/src/api/device/DeviceRebootToBoardloader.ts +10 -1
  278. package/src/api/device/DeviceRebootToBootloader.ts +10 -1
  279. package/src/api/device/DeviceSupportFeatures.ts +2 -13
  280. package/src/api/device/DeviceUpdateBootloader.ts +4 -4
  281. package/src/api/device/DeviceUploadResource.ts +4 -5
  282. package/src/api/device/DeviceVerify.ts +1 -2
  283. package/src/api/dynex/DnxGetAddress.ts +6 -0
  284. package/src/api/dynex/DnxSignTransaction.ts +6 -0
  285. package/src/api/evm/EVMGetAddress.ts +1 -1
  286. package/src/api/evm/EVMGetPublicKey.ts +3 -4
  287. package/src/api/evm/EVMSignMessage.ts +1 -1
  288. package/src/api/evm/EVMSignMessageEIP712.ts +14 -1
  289. package/src/api/evm/EVMSignTransaction.ts +1 -1
  290. package/src/api/evm/EVMSignTypedData.ts +32 -18
  291. package/src/api/evm/EVMVerifyMessage.ts +1 -1
  292. package/src/api/evm/latest/signTypedHash.ts +2 -4
  293. package/src/api/evm/legacyV1/getAddress.ts +5 -3
  294. package/src/api/evm/legacyV1/getPublicKey.ts +5 -3
  295. package/src/api/evm/legacyV1/signMessage.ts +5 -3
  296. package/src/api/evm/legacyV1/signTypedData.ts +9 -8
  297. package/src/api/evm/legacyV1/signTypedHash.ts +7 -7
  298. package/src/api/evm/legacyV1/verifyMessage.ts +5 -3
  299. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +8 -8
  300. package/src/api/firmware/uploadFirmware.ts +3 -10
  301. package/src/api/helpers/batchGetPublickeys.ts +51 -6
  302. package/src/api/helpers/filesystemValidation.ts +51 -0
  303. package/src/api/index.ts +34 -0
  304. package/src/api/kaspa/KaspaSignTransaction.ts +4 -5
  305. package/src/api/lightning/LnurlAuth.ts +4 -0
  306. package/src/api/neo/NeoGetAddress.ts +6 -1
  307. package/src/api/neo/NeoSignTransaction.ts +6 -1
  308. package/src/api/nervos/NervosGetAddress.ts +6 -2
  309. package/src/api/nervos/NervosSignTransaction.ts +14 -4
  310. package/src/api/nexa/NexaGetAddress.ts +6 -2
  311. package/src/api/nexa/NexaSignTransaction.ts +11 -12
  312. package/src/api/polkadot/networks.ts +9 -0
  313. package/src/api/protocol-v2/DevReboot.ts +24 -0
  314. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +63 -0
  315. package/src/api/protocol-v2/DeviceGetDeviceInfo.ts +118 -0
  316. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +20 -0
  317. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +16 -0
  318. package/src/api/protocol-v2/DeviceReboot.ts +24 -0
  319. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +29 -0
  320. package/src/api/protocol-v2/FactoryGetDeviceInfo.ts +20 -0
  321. package/src/api/protocol-v2/FilesystemDiskControl.ts +50 -0
  322. package/src/api/protocol-v2/FilesystemFixPermission.ts +16 -0
  323. package/src/api/protocol-v2/FilesystemFormat.ts +16 -0
  324. package/src/api/protocol-v2/GetProtoVersion.ts +16 -0
  325. package/src/api/protocol-v2/Ping.ts +18 -0
  326. package/src/api/protocol-v2/helpers.ts +207 -0
  327. package/src/api/scdo/ScdoGetAddress.ts +6 -2
  328. package/src/api/scdo/ScdoSignMessage.ts +6 -1
  329. package/src/api/scdo/ScdoSignTransaction.ts +6 -2
  330. package/src/api/solana/SolGetAddress.ts +4 -0
  331. package/src/api/solana/SolSignMessage.ts +3 -0
  332. package/src/api/solana/SolSignOffchainMessage.ts +3 -0
  333. package/src/api/solana/SolSignTransaction.ts +6 -0
  334. package/src/api/stellar/StellarGetAddress.ts +10 -1
  335. package/src/api/stellar/StellarSignTransaction.ts +14 -1
  336. package/src/api/sui/SuiGetAddress.ts +5 -3
  337. package/src/api/sui/SuiGetPublicKey.ts +3 -0
  338. package/src/api/sui/SuiSignMessage.ts +3 -0
  339. package/src/api/sui/SuiSignTransaction.ts +14 -12
  340. package/src/api/ton/TonGetAddress.ts +3 -0
  341. package/src/api/ton/TonSignData.ts +10 -3
  342. package/src/api/ton/TonSignMessage.ts +6 -5
  343. package/src/api/ton/TonSignProof.ts +3 -0
  344. package/src/api/tron/TronSignMessage.ts +5 -1
  345. package/src/api/tron/TronSignTransaction.ts +4 -0
  346. package/src/api/xrp/XrpSignTransaction.ts +1 -1
  347. package/src/core/index.ts +87 -55
  348. package/src/data/messages/messages-protocol-v2.json +13369 -0
  349. package/src/data-manager/DataManager.ts +12 -7
  350. package/src/data-manager/MessagesConfig.ts +14 -14
  351. package/src/data-manager/TransportManager.ts +38 -12
  352. package/src/device/Device.ts +538 -61
  353. package/src/device/DeviceCommands.ts +195 -29
  354. package/src/device/DeviceConnector.ts +29 -4
  355. package/src/device/DevicePool.ts +7 -7
  356. package/src/deviceProfile/buildDeviceProfile.ts +387 -0
  357. package/src/deviceProfile/index.ts +2 -0
  358. package/src/deviceProfile/legacyFeaturesView.ts +123 -0
  359. package/src/events/logBlockEvent.ts +23 -0
  360. package/src/events/ui-request.ts +8 -0
  361. package/src/inject.ts +52 -1
  362. package/src/protocols/protocol-v2/features.ts +180 -0
  363. package/src/protocols/protocol-v2/firmware.ts +43 -0
  364. package/src/protocols/protocol-v2/index.ts +16 -0
  365. package/src/types/api/export.ts +1 -0
  366. package/src/types/api/firmwareUpdate.ts +49 -0
  367. package/src/types/api/getDeviceInfo.ts +99 -0
  368. package/src/types/api/getPassphraseState.ts +13 -2
  369. package/src/types/api/index.ts +88 -1
  370. package/src/types/api/protocolV2.ts +201 -0
  371. package/src/types/api/searchDevices.ts +2 -2
  372. package/src/types/device.ts +37 -3
  373. package/src/types/params.ts +7 -0
  374. package/src/types/settings.ts +1 -1
  375. package/src/utils/deviceFeaturesUtils.ts +70 -70
  376. package/src/utils/deviceInfoUtils.ts +15 -8
  377. package/src/utils/index.ts +1 -0
@@ -0,0 +1,3025 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { DevRebootType } from '@onekeyfe/hd-transport';
3
+
4
+ import ConfluxSignTransaction from '../src/api/conflux/ConfluxSignTransaction';
5
+ import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
6
+ import DnxSignTransaction from '../src/api/dynex/DnxSignTransaction';
7
+ import DirList from '../src/api/DirList';
8
+ import FileRead from '../src/api/FileRead';
9
+ import FileWrite from '../src/api/FileWrite';
10
+ import DevReboot from '../src/api/protocol-v2/DevReboot';
11
+ import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
12
+ import DeviceGetDeviceInfo from '../src/api/protocol-v2/DeviceGetDeviceInfo';
13
+ import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus';
14
+ import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
15
+ import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
16
+ import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
17
+ import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
18
+ import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4';
19
+ import GetDeviceInfo from '../src/api/GetDeviceInfo';
20
+ import GetPassphraseState from '../src/api/GetPassphraseState';
21
+ import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures';
22
+ import { batchGetPublickeys } from '../src/api/helpers/batchGetPublickeys';
23
+ import LnurlAuth from '../src/api/lightning/LnurlAuth';
24
+ import PolkadotGetAddress from '../src/api/polkadot/PolkadotGetAddress';
25
+ import SuiGetAddress from '../src/api/sui/SuiGetAddress';
26
+ import SuiGetPublicKey from '../src/api/sui/SuiGetPublicKey';
27
+ import SuiSignMessage from '../src/api/sui/SuiSignMessage';
28
+ import SuiSignTransaction from '../src/api/sui/SuiSignTransaction';
29
+ import SolGetAddress from '../src/api/solana/SolGetAddress';
30
+ import SolSignMessage from '../src/api/solana/SolSignMessage';
31
+ import SolSignOffchainMessage from '../src/api/solana/SolSignOffchainMessage';
32
+ import SolSignTransaction from '../src/api/solana/SolSignTransaction';
33
+ import TonGetAddress from '../src/api/ton/TonGetAddress';
34
+ import TonSignData from '../src/api/ton/TonSignData';
35
+ import TonSignMessage from '../src/api/ton/TonSignMessage';
36
+ import TonSignProof from '../src/api/ton/TonSignProof';
37
+ import TronGetAddress from '../src/api/tron/TronGetAddress';
38
+ import TronSignMessage from '../src/api/tron/TronSignMessage';
39
+ import XrpSignTransaction from '../src/api/xrp/XrpSignTransaction';
40
+ import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
41
+ import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
42
+ import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit';
43
+ import { DataManager } from '../src/data-manager';
44
+ import { Device } from '../src/device/Device';
45
+ import { UI_REQUEST } from '../src/events/ui-request';
46
+ import {
47
+ PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
48
+ buildMockProtocolV2DeviceInfo,
49
+ requestProtocolV2DeviceInfo,
50
+ setProtocolV2DeviceInfoMock,
51
+ } from '../src/protocols/protocol-v2/features';
52
+ import {
53
+ buildProfileFromProtocolV2,
54
+ buildProtocolV2GetFeaturesPayload,
55
+ } from '../src/deviceProfile';
56
+ import {
57
+ getDeviceType,
58
+ getFirmwareType,
59
+ getMethodVersionRange,
60
+ isMethodVersionRangeUnsupported,
61
+ } from '../src/utils';
62
+ import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils';
63
+ import {
64
+ getPassphraseState,
65
+ getPassphraseStateWithRefreshDeviceInfo,
66
+ } from '../src/utils/deviceFeaturesUtils';
67
+
68
+ import type { DeviceCommands } from '../src/device/DeviceCommands';
69
+ import type { Features } from '../src/types';
70
+ import type { ProtocolV2DeviceInfo } from '../src/protocols/protocol-v2/features';
71
+
72
+ jest.mock('../src/data/config', () => ({
73
+ getSDKVersion: jest.fn(() => '1.0.0'),
74
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
75
+ }));
76
+
77
+ // DeviceGetDeviceInfo mock 开关默认开启(早期工程板固件没有该消息)。
78
+ // 本测试文件验证的是真实 wire 调用行为,统一关闭;mock 行为单独有用例覆盖。
79
+ beforeAll(() => {
80
+ setProtocolV2DeviceInfoMock(false);
81
+ });
82
+
83
+ const descriptor = {
84
+ id: 'ble-id',
85
+ path: 'usb-path',
86
+ };
87
+
88
+ /**
89
+ * 为纯对象 stub 设备补齐 Device 的协议判别与 getCurrent* accessor,
90
+ * 缺省实现与 Device.ts 语义保持一致;已有同名字段(如 jest.fn())不覆盖。
91
+ */
92
+ function stubDevice<T extends Record<string, any>>(device: T): T {
93
+ const d = device as any;
94
+ d.isProtocolV2 ??= () =>
95
+ d.profile?.protocol === 'V2' || d.originalDescriptor?.protocolType === 'V2';
96
+ d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1');
97
+ d.getCurrentDeviceType ??= () => d.profile?.deviceType ?? getDeviceType(d.features);
98
+ d.getCurrentFirmwareType ??= () => d.profile?.firmwareType ?? getFirmwareType(d.features);
99
+ d.getCurrentFirmwareVersionString ??= () =>
100
+ d.profile?.versions?.firmware ??
101
+ (d.features ? getDeviceFirmwareVersion(d.features).join('.') : undefined);
102
+ d.getCurrentBLEFirmwareVersionString ??= () => d.profile?.versions?.ble ?? undefined;
103
+ d.getCurrentSafetyChecks ??= () => d.features?.safety_checks;
104
+ d.getCurrentDeviceId ??= () => d.profile?.deviceId || d.features?.device_id || undefined;
105
+ d.getCurrentSerialNo ??= () => d.profile?.serialNo || '';
106
+ d.getCurrentPassphraseProtection ??= () =>
107
+ d.profile ? d.profile.status?.passphraseProtection : d.features?.passphrase_protection;
108
+ d.getCurrentMethodVersionRange ??= (fn: (model: any) => any) => {
109
+ const deviceType = d.getCurrentDeviceType();
110
+ const range = fn(deviceType);
111
+ if (range) return range;
112
+ return getMethodVersionRange(d.features, fn);
113
+ };
114
+ d.updateProfile ??= (p: any) => {
115
+ d.profile = p;
116
+ };
117
+ d.applyProfileUpdate ??= (p: any) => {
118
+ d.profile = p;
119
+ return p;
120
+ };
121
+ return device;
122
+ }
123
+
124
+ // 直接复用生产映射函数,避免测试内副本与实现漂移(之前的手抄副本已缺失 se boot 字段)
125
+ function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: ProtocolV2DeviceInfo) {
126
+ const profile = buildProfileFromProtocolV2({
127
+ deviceInfo,
128
+ sources: ['deviceInfo'],
129
+ scope: 'verify',
130
+ });
131
+ return buildProtocolV2GetFeaturesPayload(profile, deviceInfo);
132
+ }
133
+
134
+ async function requestProtocolV2LegacyFeatures({
135
+ commands,
136
+ descriptor: inputDescriptor,
137
+ }: {
138
+ commands: DeviceCommands;
139
+ descriptor?: unknown;
140
+ }) {
141
+ const deviceInfo = await requestProtocolV2DeviceInfo({ commands });
142
+ return normalizeProtocolV2Features(inputDescriptor ?? descriptor, deviceInfo);
143
+ }
144
+
145
+ describe('Protocol V2 feature adapter', () => {
146
+ test('skips DeviceGetDeviceInfo wire call and returns mock when mock is enabled', async () => {
147
+ const typedCall = jest.fn();
148
+ setProtocolV2DeviceInfoMock(true);
149
+ try {
150
+ const deviceInfo = await requestProtocolV2DeviceInfo({
151
+ commands: { typedCall } as unknown as DeviceCommands,
152
+ });
153
+ expect(typedCall).not.toHaveBeenCalled();
154
+ expect(deviceInfo).toEqual(buildMockProtocolV2DeviceInfo());
155
+ // 空 serial_no + fallbackSerialNo:身份字段回退 transport path
156
+ const profile = buildProfileFromProtocolV2({
157
+ deviceInfo,
158
+ fallbackSerialNo: 'usb-path',
159
+ });
160
+ expect(profile).toMatchObject({
161
+ protocol: 'V2',
162
+ deviceId: 'usb-path',
163
+ serialNo: 'usb-path',
164
+ status: { initialized: true },
165
+ versions: { firmware: '0.1.0' },
166
+ });
167
+ } finally {
168
+ setProtocolV2DeviceInfoMock(false);
169
+ }
170
+ });
171
+
172
+ test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
173
+ const features = normalizeProtocolV2Features(descriptor as any, {
174
+ protocol_version: 1,
175
+ hw: {
176
+ serial_no: 'PR2SERIAL',
177
+ },
178
+ fw: {
179
+ board: {
180
+ version: '0.1.0',
181
+ hash: [1, 2, 255],
182
+ },
183
+ boot: {
184
+ version: '0.2.0',
185
+ build_id: 'boot-build',
186
+ hash: new Uint8Array([10, 11]),
187
+ },
188
+ app: {
189
+ version: '1.2.3',
190
+ build_id: 'app-build',
191
+ hash: 'abc123',
192
+ },
193
+ },
194
+ bt: {
195
+ app: {
196
+ version: '4.5.6',
197
+ build_id: 'bt-build',
198
+ hash: [12, 13],
199
+ },
200
+ adv_name: 'Pro2 BLE',
201
+ },
202
+ se1: {
203
+ app: {
204
+ version: '7.8.9',
205
+ build_id: 'se-build',
206
+ hash: [14, 15],
207
+ },
208
+ state: 85,
209
+ },
210
+ se2: {
211
+ app: {
212
+ version: '8.0.0',
213
+ },
214
+ state: 0,
215
+ },
216
+ status: {
217
+ label: 'My Pro2',
218
+ language: 'en-US',
219
+ bt_enable: true,
220
+ init_states: false,
221
+ backup_required: true,
222
+ passphrase_protection: true,
223
+ },
224
+ });
225
+
226
+ expect(features.device_id).toBe('PR2SERIAL');
227
+ expect(features.serial_no).toBe('PR2SERIAL');
228
+ expect(features.onekey_serial_no).toBe('PR2SERIAL');
229
+ expect(features.onekey_device_type).toBe('pro2');
230
+ expect(features.protocol_version).toBe(1);
231
+ expect(features.major_version).toBe(1);
232
+ expect(features.minor_version).toBe(2);
233
+ expect(features.patch_version).toBe(3);
234
+ expect(features.onekey_firmware_version).toBe('1.2.3');
235
+ expect(features.onekey_firmware_build_id).toBe('app-build');
236
+ expect(features.onekey_firmware_hash).toBe('abc123');
237
+ expect(features.bootloader_version).toBe('0.2.0');
238
+ expect(features.onekey_boot_build_id).toBe('boot-build');
239
+ expect(features.onekey_boot_hash).toBe('0a0b');
240
+ expect(features.onekey_board_hash).toBe('0102ff');
241
+ expect(features.ble_name).toBe('Pro2 BLE');
242
+ expect(features.onekey_ble_version).toBe('4.5.6');
243
+ expect(features.onekey_ble_hash).toBe('0c0d');
244
+ expect(features.onekey_se01_version).toBe('7.8.9');
245
+ expect(features.onekey_se01_hash).toBe('0e0f');
246
+ expect(features.onekey_se01_state).toBe('APP');
247
+ expect(features.onekey_se02_state).toBe('BOOT');
248
+ expect(features.label).toBe('My Pro2');
249
+ expect(features.language).toBe('en-US');
250
+ expect(features.initialized).toBe(false);
251
+ expect(features.needs_backup).toBe(true);
252
+ expect(features.passphrase_protection).toBe(true);
253
+ expect(features.ble_enable).toBe(true);
254
+ });
255
+
256
+ test('uses GetPassphraseState payloads compatible with Pro series passphrase flow', async () => {
257
+ const features = normalizeProtocolV2Features(descriptor as any);
258
+ // Pro2 版本线独立于 Pro 系列:V2 设备无论固件版本都支持 GetPassphraseState
259
+ features.onekey_firmware_version = '1.2.3';
260
+ const typedCall = jest.fn().mockResolvedValue({
261
+ type: 'PassphraseState',
262
+ message: {
263
+ passphrase_state: 'state-1',
264
+ session_id: 'session-1',
265
+ unlocked_attach_pin: false,
266
+ },
267
+ });
268
+ const commands = { typedCall } as unknown as DeviceCommands;
269
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
270
+ (device as any).features = features;
271
+ (device as any).commands = commands;
272
+
273
+ await getPassphraseState(device, {
274
+ expectPassphraseState: 'state-1',
275
+ });
276
+ expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
277
+ passphrase_state: 'state-1',
278
+ });
279
+
280
+ await getPassphraseState(device, {
281
+ expectPassphraseState: 'state-2',
282
+ allowCreateAttachPin: true,
283
+ });
284
+ expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
285
+ passphrase_state: 'state-2',
286
+ allow_create_attach_pin: true,
287
+ });
288
+
289
+ await getPassphraseState(device, {
290
+ onlyMainPin: true,
291
+ });
292
+ expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
293
+ _only_main_pin: true,
294
+ });
295
+ });
296
+
297
+ test('returns unified GetPassphraseState object payload for existing Pro devices', async () => {
298
+ const features = {
299
+ device_id: 'pro-device-id',
300
+ onekey_device_type: 'PRO',
301
+ onekey_firmware_version: '4.15.0',
302
+ passphrase_protection: true,
303
+ session_id: 'feature-session',
304
+ unlocked_attach_pin: true,
305
+ };
306
+ const typedCall = jest.fn().mockResolvedValue({
307
+ type: 'PassphraseState',
308
+ message: {
309
+ passphrase_state: 'state-pro',
310
+ session_id: 'session-pro',
311
+ unlocked_attach_pin: false,
312
+ },
313
+ });
314
+ const updateInternalState = jest.fn();
315
+ const method = new GetPassphraseState({
316
+ payload: {
317
+ method: 'getPassphraseState',
318
+ connectId: 'connect-id',
319
+ },
320
+ });
321
+ method.device = stubDevice({
322
+ features,
323
+ commands: { typedCall },
324
+ updateInternalState,
325
+ getCurrentDeviceId: () => 'pro-device-id',
326
+ getCurrentPassphraseProtection: () => true,
327
+ }) as any;
328
+
329
+ await expect(method.run()).resolves.toEqual({
330
+ passphrase_state: 'state-pro',
331
+ session_id: 'session-pro',
332
+ unlocked_attach_pin: false,
333
+ passphrase_protection: true,
334
+ });
335
+ expect(updateInternalState).toHaveBeenCalledWith(
336
+ true,
337
+ 'state-pro',
338
+ 'pro-device-id',
339
+ 'session-pro',
340
+ 'feature-session'
341
+ );
342
+ });
343
+
344
+ test('prefers DeviceProfile for GetPassphraseState response metadata', async () => {
345
+ const features = {
346
+ device_id: 'legacy-pro-device-id',
347
+ onekey_device_type: 'PRO',
348
+ onekey_firmware_version: '4.15.0',
349
+ passphrase_protection: false,
350
+ session_id: 'feature-session',
351
+ unlocked_attach_pin: true,
352
+ };
353
+ const typedCall = jest.fn().mockResolvedValue({
354
+ type: 'PassphraseState',
355
+ message: {
356
+ passphrase_state: 'state-pro2',
357
+ session_id: 'session-pro2',
358
+ unlocked_attach_pin: false,
359
+ },
360
+ });
361
+ const updateInternalState = jest.fn();
362
+ const getFeatures = jest.fn().mockResolvedValue(features);
363
+ const method = new GetPassphraseState({
364
+ payload: {
365
+ method: 'getPassphraseState',
366
+ connectId: 'connect-id',
367
+ },
368
+ });
369
+ method.device = stubDevice({
370
+ features,
371
+ profile: {
372
+ protocol: 'V2',
373
+ sources: ['deviceInfo'],
374
+ deviceType: 'pro2',
375
+ firmwareType: 'universal',
376
+ deviceId: 'PR2SERIAL',
377
+ serialNo: 'PR2SERIAL',
378
+ label: null,
379
+ bleName: null,
380
+ status: {
381
+ mode: 'normal',
382
+ initialized: true,
383
+ bootloaderMode: false,
384
+ unlocked: null,
385
+ passphraseProtection: true,
386
+ backupRequired: false,
387
+ noBackup: null,
388
+ language: null,
389
+ },
390
+ versions: {
391
+ firmware: '4.15.0',
392
+ bootloader: null,
393
+ board: null,
394
+ ble: null,
395
+ },
396
+ },
397
+ commands: { typedCall },
398
+ updateInternalState,
399
+ getFeatures,
400
+ getCurrentDeviceType: () => 'pro2',
401
+ getCurrentDeviceId: () => 'PR2SERIAL',
402
+ getCurrentPassphraseProtection: () => true,
403
+ }) as any;
404
+
405
+ await expect(method.run()).resolves.toEqual({
406
+ passphrase_state: 'state-pro2',
407
+ session_id: 'session-pro2',
408
+ unlocked_attach_pin: false,
409
+ passphrase_protection: true,
410
+ });
411
+ expect(getFeatures).not.toHaveBeenCalled();
412
+ });
413
+
414
+ test('uses DeviceProfile identity for Pro2 passphrase session cache', async () => {
415
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
416
+ const typedCall = jest.fn().mockResolvedValue({
417
+ type: 'PassphraseState',
418
+ message: {
419
+ passphrase_state: 'state-profile',
420
+ session_id: 'session-profile',
421
+ unlocked_attach_pin: false,
422
+ },
423
+ });
424
+
425
+ (device as any).features = {
426
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
427
+ hw: { serial_no: 'PR2SERIAL' },
428
+ fw: { app: { version: '4.15.0' } },
429
+ status: { passphrase_protection: true },
430
+ }),
431
+ unlocked: true,
432
+ };
433
+ (device as any).commands = { typedCall };
434
+
435
+ await getPassphraseStateWithRefreshDeviceInfo(device);
436
+
437
+ device.passphraseState = 'state-profile';
438
+ expect(device.getInternalState()).toBe('session-profile');
439
+ expect(device.getInternalState('LEGACY-ID')).toBeUndefined();
440
+ });
441
+
442
+ test('stores Pro2 passphrase sessions without selecting them implicitly', async () => {
443
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
444
+ const typedCall = jest.fn().mockResolvedValue({
445
+ type: 'PassphraseState',
446
+ message: {
447
+ passphrase_state: 'state-auto',
448
+ session_id: 'session-auto',
449
+ unlocked_attach_pin: false,
450
+ },
451
+ });
452
+
453
+ (device as any).features = normalizeProtocolV2Features(
454
+ {
455
+ ...descriptor,
456
+ protocolType: 'V2',
457
+ } as any,
458
+ {
459
+ hw: { serial_no: 'PR2SERIAL' },
460
+ }
461
+ );
462
+ (device as any).features.onekey_firmware_version = '4.15.0';
463
+ (device as any).features.passphrase_protection = true;
464
+ (device as any).features.unlocked = true;
465
+ (device as any).commands = { typedCall };
466
+
467
+ await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({
468
+ passphraseState: 'state-auto',
469
+ newSession: 'session-auto',
470
+ });
471
+
472
+ expect(device.passphraseState).toBeUndefined();
473
+ expect(device.features?.passphrase_protection).toBe(true);
474
+ expect(device.features?.session_id).toBeNull();
475
+ expect(device.getInternalState()).toBeUndefined();
476
+ device.passphraseState = 'state-auto';
477
+ expect(device.getInternalState()).toBe('session-auto');
478
+ expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
479
+ passphrase_state: undefined,
480
+ });
481
+ });
482
+
483
+ test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
484
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
485
+ const typedCall = jest.fn().mockResolvedValue({
486
+ type: 'PassphraseState',
487
+ message: {
488
+ session_id: 'main-pin-session',
489
+ unlocked_attach_pin: false,
490
+ },
491
+ });
492
+
493
+ (device as any).features = normalizeProtocolV2Features(
494
+ {
495
+ ...descriptor,
496
+ protocolType: 'V2',
497
+ } as any,
498
+ {
499
+ status: {
500
+ passphrase_protection: false,
501
+ },
502
+ }
503
+ );
504
+ (device as any).features.onekey_firmware_version = '4.15.0';
505
+ (device as any).features.unlocked = true;
506
+ (device as any).commands = { typedCall };
507
+
508
+ await expect(
509
+ getPassphraseStateWithRefreshDeviceInfo(device, { onlyMainPin: true })
510
+ ).resolves.toMatchObject({
511
+ passphraseState: undefined,
512
+ newSession: 'main-pin-session',
513
+ });
514
+
515
+ expect(device.features?.passphrase_protection).toBe(false);
516
+ expect(device.features?.session_id).toBeNull();
517
+ expect(device.getInternalState()).toBeUndefined();
518
+ expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
519
+ _only_main_pin: true,
520
+ });
521
+ });
522
+
523
+ test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
524
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
525
+ const typedCall = jest.fn().mockResolvedValue({
526
+ type: 'PassphraseState',
527
+ message: {
528
+ passphrase_state: 'wrong-state',
529
+ session_id: 'wrong-session',
530
+ unlocked_attach_pin: false,
531
+ },
532
+ });
533
+
534
+ (device as any).features = normalizeProtocolV2Features({
535
+ ...descriptor,
536
+ protocolType: 'V2',
537
+ } as any);
538
+ (device as any).features.onekey_firmware_version = '4.15.0';
539
+ (device as any).features.passphrase_protection = true;
540
+ (device as any).commands = { typedCall };
541
+
542
+ await expect(device.checkPassphraseStateSafety('expected-state', false, true)).resolves.toBe(
543
+ false
544
+ );
545
+
546
+ expect(device.getInternalState()).toBeUndefined();
547
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'GetPassphraseState', 'PassphraseState', {
548
+ passphrase_state: 'expected-state',
549
+ });
550
+ });
551
+
552
+ test('marks fallback features as unavailable when DeviceInfo is missing', () => {
553
+ const features = normalizeProtocolV2Features(descriptor as any);
554
+
555
+ expect(features.device_id).toBe('');
556
+ expect(features.serial_no).toBe('');
557
+ expect(features.onekey_serial_no).toBe('');
558
+ expect(features.initialized).toBe(false);
559
+ expect(features.unlocked).toBe(false);
560
+ expect(features.firmware_present).toBe(false);
561
+ });
562
+
563
+ test('uses Protocol V2 DeviceInfo serial_no as device_id instead of descriptor id', () => {
564
+ const features = normalizeProtocolV2Features(
565
+ {
566
+ id: 'PR2000000000',
567
+ path: 'PR2000000000',
568
+ protocolType: 'V2',
569
+ } as any,
570
+ {
571
+ hw: {
572
+ serial_no: 'PR9999999999',
573
+ },
574
+ }
575
+ );
576
+
577
+ expect(features.device_id).toBe('PR9999999999');
578
+ expect(features.onekey_serial_no).toBe('PR9999999999');
579
+ expect(features.serial_no).toBe('PR9999999999');
580
+ });
581
+
582
+ test('does not expose legacy feature ids when Protocol V2 profile is incomplete', () => {
583
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
584
+ (device as any).features = {
585
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
586
+ device_id: 'LEGACY-ID',
587
+ serial_no: 'LEGACY-SERIAL',
588
+ label: 'Legacy Label',
589
+ ble_name: 'Legacy BLE',
590
+ passphrase_protection: true,
591
+ };
592
+ device.updateProfile({
593
+ protocol: 'V2',
594
+ sources: ['deviceInfo'],
595
+ deviceType: 'pro2',
596
+ firmwareType: 'universal',
597
+ deviceId: '',
598
+ serialNo: '',
599
+ label: null,
600
+ bleName: null,
601
+ status: {
602
+ mode: 'normal',
603
+ initialized: true,
604
+ bootloaderMode: false,
605
+ unlocked: null,
606
+ passphraseProtection: null,
607
+ backupRequired: false,
608
+ noBackup: null,
609
+ language: null,
610
+ },
611
+ versions: {
612
+ firmware: null,
613
+ bootloader: null,
614
+ board: null,
615
+ ble: null,
616
+ },
617
+ });
618
+
619
+ expect(device.toMessageObject()).toMatchObject({
620
+ uuid: '',
621
+ deviceId: null,
622
+ bleName: null,
623
+ label: 'OneKey Pro2',
624
+ deviceType: 'pro2',
625
+ });
626
+ expect(device.getCurrentPassphraseProtection()).toBeNull();
627
+ expect(device.hasUsePassphrase()).toBe(true);
628
+ expect(device.isInitialized()).toBe(true);
629
+ expect(device.getMode()).toBe('normal');
630
+ expect(device.getFirmwareVersion()).toBeNull();
631
+ });
632
+
633
+ test('keeps Protocol V2 cached profile as identity source when syncing legacy features', () => {
634
+ const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
635
+ (cached as any).features = {
636
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
637
+ device_id: 'LEGACY-ID',
638
+ serial_no: 'LEGACY-SERIAL',
639
+ onekey_serial_no: 'LEGACY-SERIAL',
640
+ };
641
+ cached.updateProfile({
642
+ protocol: 'V2',
643
+ sources: ['deviceInfo'],
644
+ deviceType: 'pro2',
645
+ firmwareType: 'universal',
646
+ deviceId: '',
647
+ serialNo: '',
648
+ label: null,
649
+ bleName: null,
650
+ status: {
651
+ mode: 'normal',
652
+ initialized: true,
653
+ bootloaderMode: false,
654
+ unlocked: null,
655
+ passphraseProtection: null,
656
+ backupRequired: false,
657
+ noBackup: null,
658
+ language: null,
659
+ },
660
+ versions: {
661
+ firmware: null,
662
+ bootloader: null,
663
+ board: null,
664
+ ble: null,
665
+ },
666
+ });
667
+
668
+ const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
669
+ current.updateFromCache(cached);
670
+
671
+ expect(current.getCurrentDeviceId()).toBeUndefined();
672
+ expect(current.getCurrentSerialNo()).toBe('');
673
+ expect(current.toMessageObject()).toMatchObject({
674
+ uuid: '',
675
+ deviceId: null,
676
+ });
677
+ });
678
+
679
+ test('initializes Protocol V2 features from lightweight DeviceGetDeviceInfo', async () => {
680
+ const commands = {
681
+ typedCall: jest.fn().mockResolvedValueOnce({
682
+ type: 'DeviceInfo',
683
+ message: {
684
+ hw: { serial_no: 'PR2SERIAL' },
685
+ status: {
686
+ init_states: true,
687
+ passphrase_protection: true,
688
+ },
689
+ },
690
+ }),
691
+ };
692
+
693
+ const features = await requestProtocolV2LegacyFeatures({
694
+ commands: commands as unknown as DeviceCommands,
695
+ descriptor: descriptor as any,
696
+ });
697
+
698
+ expect(features.device_id).toBe('PR2SERIAL');
699
+ expect(features.initialized).toBe(true);
700
+ expect(features.passphrase_protection).toBe(true);
701
+ expect(commands.typedCall).toHaveBeenCalledTimes(1);
702
+ expect(commands.typedCall).toHaveBeenNthCalledWith(
703
+ 1,
704
+ 'DevGetDeviceInfo',
705
+ 'DeviceInfo',
706
+ {
707
+ targets: {
708
+ hw: true,
709
+ fw: true,
710
+ bt: true,
711
+ status: true,
712
+ },
713
+ types: {
714
+ version: true,
715
+ specific: true,
716
+ },
717
+ },
718
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
719
+ );
720
+ });
721
+
722
+ test('fails initialization when Protocol V2 DeviceGetDeviceInfo fails', async () => {
723
+ const commands = {
724
+ typedCall: jest.fn().mockRejectedValueOnce(new Error('DeviceInfo not supported')),
725
+ };
726
+
727
+ await expect(
728
+ requestProtocolV2LegacyFeatures({
729
+ commands: commands as unknown as DeviceCommands,
730
+ descriptor: descriptor as any,
731
+ })
732
+ ).rejects.toThrow('DeviceInfo not supported');
733
+ });
734
+
735
+ test('refreshes Protocol V2 basic device info with the lightweight request', async () => {
736
+ const typedCall = jest.fn().mockResolvedValueOnce({
737
+ type: 'DeviceInfo',
738
+ message: {
739
+ hw: { serial_no: 'PR2SERIAL' },
740
+ fw: {
741
+ app: {
742
+ version: '5.6.7',
743
+ },
744
+ },
745
+ bt: {
746
+ app: {
747
+ version: '8.9.10',
748
+ },
749
+ adv_name: 'Raw Pro2 BLE',
750
+ },
751
+ status: {
752
+ init_states: true,
753
+ label: 'Raw Pro2',
754
+ passphrase_protection: true,
755
+ },
756
+ },
757
+ });
758
+ const method = new GetDeviceInfo({
759
+ id: 1,
760
+ payload: {
761
+ method: 'getDeviceInfo',
762
+ scope: 'basic',
763
+ refresh: true,
764
+ },
765
+ });
766
+ method.init();
767
+ (method as any).device = stubDevice({
768
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
769
+ features: {
770
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
771
+ device_id: 'LEGACY-ID',
772
+ serial_no: 'LEGACY-SERIAL',
773
+ label: 'Legacy Pro2',
774
+ onekey_firmware_version: '1.2.3',
775
+ onekey_ble_version: '2.3.4',
776
+ },
777
+ commands: { typedCall },
778
+ _updateFeatures: jest.fn(),
779
+ updateProfile: jest.fn(),
780
+ });
781
+
782
+ const result = await method.run();
783
+
784
+ expect(typedCall).toHaveBeenCalledWith(
785
+ 'DevGetDeviceInfo',
786
+ 'DeviceInfo',
787
+ {
788
+ targets: {
789
+ hw: true,
790
+ fw: true,
791
+ bt: true,
792
+ status: true,
793
+ },
794
+ types: {
795
+ version: true,
796
+ specific: true,
797
+ },
798
+ },
799
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
800
+ );
801
+ expect(result).toMatchObject({
802
+ protocol: 'V2',
803
+ deviceType: 'pro2',
804
+ deviceId: 'PR2SERIAL',
805
+ serialNo: 'PR2SERIAL',
806
+ label: 'Raw Pro2',
807
+ bleName: 'Raw Pro2 BLE',
808
+ status: {
809
+ initialized: true,
810
+ passphraseProtection: true,
811
+ },
812
+ versions: {
813
+ firmware: '5.6.7',
814
+ ble: '8.9.10',
815
+ },
816
+ });
817
+ });
818
+
819
+ test('does not fill Protocol V2 DeviceProfile from legacy features', async () => {
820
+ const typedCall = jest.fn().mockResolvedValueOnce({
821
+ type: 'DeviceInfo',
822
+ message: {
823
+ status: {
824
+ init_states: true,
825
+ },
826
+ },
827
+ });
828
+ const method = new GetDeviceInfo({
829
+ id: 1,
830
+ payload: {
831
+ method: 'getDeviceInfo',
832
+ scope: 'basic',
833
+ },
834
+ });
835
+ method.init();
836
+ (method as any).device = stubDevice({
837
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
838
+ features: {
839
+ ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
840
+ device_id: 'LEGACY-ID',
841
+ serial_no: 'LEGACY-SERIAL',
842
+ label: 'Legacy Pro2',
843
+ ble_name: 'Legacy BLE',
844
+ passphrase_protection: true,
845
+ onekey_firmware_version: '1.2.3',
846
+ onekey_ble_version: '2.3.4',
847
+ },
848
+ commands: { typedCall },
849
+ updateProfile: jest.fn(),
850
+ });
851
+
852
+ const result = await method.run();
853
+
854
+ expect(result).toMatchObject({
855
+ protocol: 'V2',
856
+ // 身份字段不得取自 legacy features(LEGACY-ID / LEGACY-SERIAL);
857
+ // hw.serial_no 缺失时回退 descriptor.path(早期工程板无 serial 的 mock 身份)。
858
+ deviceId: 'usb-path',
859
+ serialNo: 'usb-path',
860
+ label: null,
861
+ bleName: null,
862
+ status: {
863
+ passphraseProtection: null,
864
+ noBackup: null,
865
+ },
866
+ versions: {
867
+ firmware: null,
868
+ ble: null,
869
+ },
870
+ });
871
+ });
872
+
873
+ test('reads full Protocol V2 device info only for verify scope', async () => {
874
+ const typedCall = jest.fn().mockResolvedValueOnce({
875
+ type: 'DeviceInfo',
876
+ message: {
877
+ hw: { serial_no: 'PR2SERIAL' },
878
+ status: { init_states: true },
879
+ },
880
+ });
881
+ const method = new GetDeviceInfo({
882
+ id: 1,
883
+ payload: {
884
+ method: 'getDeviceInfo',
885
+ scope: 'verify',
886
+ },
887
+ });
888
+ method.init();
889
+ (method as any).device = stubDevice({
890
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
891
+ features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
892
+ commands: { typedCall },
893
+ _updateFeatures: jest.fn(),
894
+ });
895
+
896
+ await method.run();
897
+
898
+ expect(typedCall).toHaveBeenCalledWith(
899
+ 'DevGetDeviceInfo',
900
+ 'DeviceInfo',
901
+ {
902
+ targets: {
903
+ hw: true,
904
+ fw: true,
905
+ bt: true,
906
+ se1: true,
907
+ se2: true,
908
+ se3: true,
909
+ se4: true,
910
+ status: true,
911
+ },
912
+ types: {
913
+ version: true,
914
+ build_id: true,
915
+ hash: true,
916
+ specific: true,
917
+ },
918
+ },
919
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
920
+ );
921
+ });
922
+
923
+ test('reads Protocol V2 SE version fields for versions scope', async () => {
924
+ const typedCall = jest.fn().mockResolvedValueOnce({
925
+ type: 'DeviceInfo',
926
+ message: {
927
+ hw: { serial_no: 'PR2SERIAL' },
928
+ se1: {
929
+ app: { version: '1.0.1' },
930
+ boot: { version: '1.0.0' },
931
+ },
932
+ se2: {
933
+ app: { version: '2.0.1' },
934
+ boot: { version: '2.0.0' },
935
+ },
936
+ status: { init_states: true },
937
+ },
938
+ });
939
+ const method = new GetDeviceInfo({
940
+ id: 1,
941
+ payload: {
942
+ method: 'getDeviceInfo',
943
+ scope: 'versions',
944
+ },
945
+ });
946
+ method.init();
947
+ (method as any).device = stubDevice({
948
+ originalDescriptor: { ...descriptor, protocolType: 'V2' },
949
+ features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
950
+ commands: { typedCall },
951
+ updateProfile: jest.fn(),
952
+ });
953
+
954
+ const result = await method.run();
955
+
956
+ expect(typedCall).toHaveBeenCalledWith(
957
+ 'DevGetDeviceInfo',
958
+ 'DeviceInfo',
959
+ {
960
+ targets: {
961
+ hw: true,
962
+ fw: true,
963
+ bt: true,
964
+ se1: true,
965
+ se2: true,
966
+ se3: true,
967
+ se4: true,
968
+ status: true,
969
+ },
970
+ types: {
971
+ version: true,
972
+ specific: true,
973
+ },
974
+ },
975
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
976
+ );
977
+ expect(result.versions).toMatchObject({
978
+ se01: '1.0.1',
979
+ se01Boot: '1.0.0',
980
+ se02: '2.0.1',
981
+ se02Boot: '2.0.0',
982
+ });
983
+ expect(result).not.toHaveProperty('verify');
984
+ });
985
+
986
+ test('does not inherit Pro or Pro model fallback ranges for Protocol V2 devices', () => {
987
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
988
+ const checkedTypes: string[] = [];
989
+
990
+ const versionRange = getMethodVersionRange(features, type => {
991
+ checkedTypes.push(type);
992
+ if (type === 'pro' || type === 'model_touch') {
993
+ return { min: '4.10.0' };
994
+ }
995
+ return undefined;
996
+ });
997
+
998
+ expect(versionRange).toBeUndefined();
999
+ expect(checkedTypes).toEqual(['pro2']);
1000
+ });
1001
+
1002
+ test('marks known unsupported public-chain methods as unsupported on Protocol V2', () => {
1003
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
1004
+ fw: {
1005
+ app: {
1006
+ version: '9.9.9',
1007
+ },
1008
+ },
1009
+ });
1010
+ const stellar = new StellarGetAddress({
1011
+ id: 1,
1012
+ payload: {
1013
+ method: 'stellarGetAddress',
1014
+ path: "m/44'/148'/0'",
1015
+ },
1016
+ });
1017
+ const benfen = new BenfenSignMessage({
1018
+ id: 1,
1019
+ payload: {
1020
+ method: 'benfenSignMessage',
1021
+ path: "m/44'/728'/0'/0'/0'",
1022
+ messageHex: '0x1234',
1023
+ },
1024
+ });
1025
+ const lnurlAuth = new LnurlAuth({
1026
+ id: 1,
1027
+ payload: {
1028
+ method: 'lnurlAuth',
1029
+ domain: 'example.com',
1030
+ k1: '1234',
1031
+ },
1032
+ });
1033
+
1034
+ stellar.init();
1035
+ benfen.init();
1036
+ lnurlAuth.init();
1037
+
1038
+ const stellarRange = getMethodVersionRange(features, type => stellar.getVersionRange()[type]);
1039
+ const benfenRange = getMethodVersionRange(features, type => benfen.getVersionRange()[type]);
1040
+ const lnurlAuthRange = getMethodVersionRange(
1041
+ features,
1042
+ type => lnurlAuth.getVersionRange()[type]
1043
+ );
1044
+ const neuraiRange = getMethodVersionRange(
1045
+ features,
1046
+ type => getBitcoinForkVersionRange(['Neurai'])[type]
1047
+ );
1048
+
1049
+ expect(isMethodVersionRangeUnsupported(stellarRange)).toBe(true);
1050
+ expect(isMethodVersionRangeUnsupported(benfenRange)).toBe(true);
1051
+ expect(isMethodVersionRangeUnsupported(lnurlAuthRange)).toBe(true);
1052
+ expect(isMethodVersionRangeUnsupported(neuraiRange)).toBe(true);
1053
+ });
1054
+
1055
+ test('does not block legacy batch public key support checks on Protocol V2', async () => {
1056
+ const paths = [{ address_n: [0x8000002c, 0x80000000, 0x80000000] }] as any;
1057
+ const typedCall = jest.fn().mockResolvedValue({
1058
+ type: 'EcdsaPublicKeys',
1059
+ message: {
1060
+ root_fingerprint: 123,
1061
+ public_keys: [],
1062
+ hd_nodes: [{}],
1063
+ },
1064
+ });
1065
+ const device = stubDevice({
1066
+ originalDescriptor: { protocolType: 'V2' },
1067
+ features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
1068
+ fw: {
1069
+ app: {
1070
+ version: '4.14.0',
1071
+ },
1072
+ },
1073
+ }),
1074
+ commands: { typedCall },
1075
+ getCurrentDeviceType: () => 'pro2',
1076
+ });
1077
+
1078
+ await expect(
1079
+ batchGetPublickeys(device as any, paths, 'secp256k1', 0, { includeNode: true })
1080
+ ).resolves.toMatchObject({
1081
+ root_fingerprint: 123,
1082
+ hd_nodes: [{}],
1083
+ });
1084
+ expect(typedCall).toHaveBeenCalledWith('BatchGetPublickeys', 'EcdsaPublicKeys', {
1085
+ paths,
1086
+ ecdsa_curve_name: 'secp256k1',
1087
+ include_node: true,
1088
+ });
1089
+ });
1090
+
1091
+ test('returns Protocol V2 oneKey fields without calling legacy OnekeyGetFeatures', async () => {
1092
+ const method = new GetOnekeyFeatures({
1093
+ id: 1,
1094
+ payload: {
1095
+ method: 'getOnekeyFeatures',
1096
+ },
1097
+ });
1098
+ const typedCall = jest.fn().mockResolvedValue({
1099
+ type: 'DeviceInfo',
1100
+ message: {
1101
+ protocol_version: 1,
1102
+ hw: { serial_no: 'PR2SERIAL' },
1103
+ fw: { app: { version: '1.2.3', build_id: 'app-build' } },
1104
+ bt: { adv_name: 'Pro2 BLE' },
1105
+ status: { label: 'ignored label' },
1106
+ },
1107
+ });
1108
+
1109
+ (method as any).device = stubDevice({
1110
+ originalDescriptor: { protocolType: 'V2' },
1111
+ commands: { typedCall },
1112
+ });
1113
+
1114
+ const message = await method.run();
1115
+
1116
+ // V2 走 DeviceGetDeviceInfo 完整请求(含 SE 与 hash/build_id),而不是 legacy OnekeyGetFeatures
1117
+ expect(typedCall).toHaveBeenCalledTimes(1);
1118
+ expect(typedCall).toHaveBeenCalledWith(
1119
+ 'DevGetDeviceInfo',
1120
+ 'DeviceInfo',
1121
+ expect.objectContaining({
1122
+ targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }),
1123
+ types: expect.objectContaining({ build_id: true, hash: true }),
1124
+ }),
1125
+ expect.anything()
1126
+ );
1127
+ expect(message).toMatchObject({
1128
+ onekey_device_type: 'pro2',
1129
+ onekey_firmware_version: '1.2.3',
1130
+ onekey_firmware_build_id: 'app-build',
1131
+ onekey_serial_no: 'PR2SERIAL',
1132
+ onekey_ble_name: 'Pro2 BLE',
1133
+ });
1134
+ expect(message).not.toHaveProperty('label');
1135
+ });
1136
+
1137
+ test('refreshes cached Protocol V2 profile with a lightweight status request on later runs', async () => {
1138
+ const device = Device.fromDescriptor({
1139
+ path: 'usb-path',
1140
+ protocolType: 'V2',
1141
+ } as any);
1142
+ const typedCall = jest
1143
+ .fn()
1144
+ .mockResolvedValueOnce({
1145
+ type: 'DeviceInfo',
1146
+ message: {
1147
+ hw: { serial_no: 'PR2SERIAL' },
1148
+ fw: { app: { version: '1.2.3' } },
1149
+ status: {
1150
+ passphrase_protection: true,
1151
+ },
1152
+ },
1153
+ })
1154
+ // 第二次 run 的轻量 status 刷新:设备端 label / init_states 等可能已变化
1155
+ .mockResolvedValueOnce({
1156
+ type: 'DeviceInfo',
1157
+ message: {
1158
+ hw: { serial_no: 'PR2SERIAL' },
1159
+ status: {
1160
+ passphrase_protection: false,
1161
+ label: 'renamed',
1162
+ },
1163
+ },
1164
+ });
1165
+
1166
+ (device as any).commands = { typedCall };
1167
+
1168
+ await device.initialize();
1169
+ await device.initialize();
1170
+
1171
+ expect(device.features).toMatchObject({
1172
+ device_id: 'PR2SERIAL',
1173
+ onekey_firmware_version: '1.2.3',
1174
+ passphrase_protection: false,
1175
+ label: 'renamed',
1176
+ });
1177
+ expect(device.profile?.deviceId).toBe('PR2SERIAL');
1178
+ // status 字段被第二次刷新更新
1179
+ expect(device.profile?.status.passphraseProtection).toBe(false);
1180
+ expect(device.profile?.label).toBe('renamed');
1181
+ // 轻量刷新不含 fw target,已有版本信息按字段级合并保留
1182
+ expect(device.profile?.versions.firmware).toBe('1.2.3');
1183
+ expect(typedCall).toHaveBeenCalledTimes(2);
1184
+ expect(typedCall).toHaveBeenNthCalledWith(
1185
+ 1,
1186
+ 'DevGetDeviceInfo',
1187
+ 'DeviceInfo',
1188
+ {
1189
+ targets: {
1190
+ hw: true,
1191
+ fw: true,
1192
+ bt: true,
1193
+ status: true,
1194
+ },
1195
+ types: {
1196
+ version: true,
1197
+ specific: true,
1198
+ },
1199
+ },
1200
+ {
1201
+ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
1202
+ }
1203
+ );
1204
+ // 第二次为 status-only 轻量请求(hw/bt 仅用于身份字段,避免顶层覆盖清空)
1205
+ expect(typedCall).toHaveBeenNthCalledWith(
1206
+ 2,
1207
+ 'DevGetDeviceInfo',
1208
+ 'DeviceInfo',
1209
+ {
1210
+ targets: {
1211
+ hw: true,
1212
+ bt: true,
1213
+ status: true,
1214
+ },
1215
+ types: {
1216
+ version: true,
1217
+ specific: true,
1218
+ },
1219
+ },
1220
+ {
1221
+ timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
1222
+ }
1223
+ );
1224
+ });
1225
+
1226
+ test('refreshes Protocol V2 features without falling back to legacy GetFeatures', async () => {
1227
+ const device = Device.fromDescriptor({
1228
+ path: 'usb-path',
1229
+ protocolType: 'V2',
1230
+ } as any);
1231
+ const typedCall = jest
1232
+ .fn()
1233
+ .mockResolvedValueOnce({
1234
+ type: 'DeviceInfo',
1235
+ message: {
1236
+ hw: { serial_no: 'PR2SERIAL' },
1237
+ fw: { app: { version: '1.2.3' } },
1238
+ status: { init_states: true },
1239
+ },
1240
+ })
1241
+ .mockResolvedValueOnce({
1242
+ type: 'DeviceInfo',
1243
+ message: {
1244
+ hw: { serial_no: 'PR2SERIAL' },
1245
+ fw: { app: { version: '1.2.4' } },
1246
+ status: { init_states: true, passphrase_protection: true },
1247
+ },
1248
+ });
1249
+
1250
+ (device as any).commands = { typedCall };
1251
+
1252
+ await device.initialize();
1253
+ const features = await device.getFeatures();
1254
+
1255
+ expect(device.features).toMatchObject({
1256
+ device_id: 'PR2SERIAL',
1257
+ onekey_firmware_version: '1.2.4',
1258
+ passphrase_protection: true,
1259
+ });
1260
+ expect(features).toMatchObject({
1261
+ onekey_device_type: 'pro2',
1262
+ onekey_serial_no: 'PR2SERIAL',
1263
+ onekey_firmware_version: '1.2.4',
1264
+ passphrase_protection: true,
1265
+ });
1266
+ expect(typedCall).toHaveBeenCalledTimes(2);
1267
+ expect(typedCall).toHaveBeenNthCalledWith(
1268
+ 2,
1269
+ 'DevGetDeviceInfo',
1270
+ 'DeviceInfo',
1271
+ {
1272
+ targets: {
1273
+ hw: true,
1274
+ fw: true,
1275
+ bt: true,
1276
+ status: true,
1277
+ },
1278
+ types: {
1279
+ version: true,
1280
+ specific: true,
1281
+ },
1282
+ },
1283
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
1284
+ );
1285
+ expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1286
+ });
1287
+
1288
+ test('keeps Protocol V2 features available for legacy method internals such as evmSignTypedData', async () => {
1289
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1290
+ const typedCall = jest.fn().mockResolvedValueOnce({
1291
+ type: 'DeviceInfo',
1292
+ message: {
1293
+ hw: { serial_no: 'PR2SERIAL' },
1294
+ fw: { app: { version: '1.2.4' } },
1295
+ status: { init_states: true, passphrase_protection: true },
1296
+ },
1297
+ });
1298
+ (device as any).commands = { typedCall };
1299
+
1300
+ await device.initialize();
1301
+ expect(device.features).toBeDefined();
1302
+
1303
+ const method = new EVMSignTypedData({
1304
+ id: 1,
1305
+ payload: {
1306
+ method: 'evmSignTypedData',
1307
+ path: "m/44'/60'/0'/0/0",
1308
+ metamaskV4Compat: true,
1309
+ data: {
1310
+ types: {
1311
+ EIP712Domain: [],
1312
+ Mail: [{ name: 'contents', type: 'string' }],
1313
+ },
1314
+ primaryType: 'Mail',
1315
+ domain: {},
1316
+ message: { contents: 'hello' },
1317
+ },
1318
+ },
1319
+ });
1320
+ method.init();
1321
+ (method as any).device = stubDevice({
1322
+ profile: device.profile,
1323
+ features: device.features,
1324
+ commands: { typedCall: jest.fn() },
1325
+ });
1326
+ (method as any).signTypedData = jest
1327
+ .fn()
1328
+ .mockResolvedValue({ address: '0x0', signature: '0x1' });
1329
+
1330
+ await expect(method.run()).resolves.toEqual({ address: '0x0', signature: '0x1' });
1331
+ });
1332
+
1333
+ test('unlocks Protocol V2 devices via UnLockDevice regardless of Pro-series version gates', async () => {
1334
+ const device = Device.fromDescriptor({
1335
+ path: 'usb-path',
1336
+ protocolType: 'V2',
1337
+ } as any);
1338
+ const typedCall = jest.fn().mockResolvedValue({
1339
+ type: 'UnLockDeviceResponse',
1340
+ message: { unlocked: true, passphrase_protection: true },
1341
+ });
1342
+
1343
+ (device as any).commands = { typedCall };
1344
+ // Pro2 版本线独立于 Pro(这里是 1.2.3,不满足 Pro 系列 4.15.0 门槛),
1345
+ // Protocol V2 固件从首个版本即支持 UnLockDevice,不走 GetAddress 探测回退。
1346
+ (device as any).features = normalizeProtocolV2Features(
1347
+ { ...descriptor, protocolType: 'V2' } as any,
1348
+ {
1349
+ hw: { serial_no: 'PR2SERIAL' },
1350
+ fw: { app: { version: '1.2.3' } },
1351
+ status: { init_states: true },
1352
+ }
1353
+ );
1354
+
1355
+ const features = await device.unlockDevice();
1356
+
1357
+ expect(typedCall).toHaveBeenCalledWith('UnLockDevice', 'UnLockDeviceResponse');
1358
+ expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything());
1359
+ expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1360
+ expect(features).toMatchObject({
1361
+ onekey_device_type: 'pro2',
1362
+ device_id: 'PR2SERIAL',
1363
+ onekey_firmware_version: '1.2.3',
1364
+ unlocked: true,
1365
+ });
1366
+ });
1367
+
1368
+ test('syncs Protocol V2 profile passphrase state after unlock response', async () => {
1369
+ const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1370
+ (device as any).features = normalizeProtocolV2Features(
1371
+ { ...descriptor, protocolType: 'V2' } as any,
1372
+ {
1373
+ hw: { serial_no: 'PR2SERIAL' },
1374
+ fw: { app: { version: '4.15.0' } },
1375
+ status: { passphrase_protection: false },
1376
+ }
1377
+ );
1378
+ device.updateProfile({
1379
+ protocol: 'V2',
1380
+ sources: ['deviceInfo'],
1381
+ deviceType: 'pro2',
1382
+ firmwareType: 'universal',
1383
+ deviceId: 'PR2SERIAL',
1384
+ serialNo: 'PR2SERIAL',
1385
+ label: null,
1386
+ bleName: null,
1387
+ status: {
1388
+ mode: 'normal',
1389
+ initialized: true,
1390
+ bootloaderMode: false,
1391
+ unlocked: null,
1392
+ passphraseProtection: false,
1393
+ backupRequired: false,
1394
+ noBackup: null,
1395
+ language: null,
1396
+ },
1397
+ versions: {
1398
+ firmware: '4.15.0',
1399
+ bootloader: null,
1400
+ board: null,
1401
+ ble: null,
1402
+ },
1403
+ });
1404
+ const typedCall = jest.fn().mockResolvedValue({
1405
+ type: 'UnLockDeviceResponse',
1406
+ message: {
1407
+ unlocked: true,
1408
+ passphrase_protection: true,
1409
+ },
1410
+ });
1411
+ (device as any).commands = { typedCall };
1412
+
1413
+ await device.unlockDevice();
1414
+
1415
+ expect(device.profile?.status.passphraseProtection).toBe(true);
1416
+ expect(device.features?.passphrase_protection).toBe(true);
1417
+ });
1418
+ });
1419
+
1420
+ describe('API compatibility handling', () => {
1421
+ test('returns a typed unsupported error for deprecated EIP712 message signing on Protocol V2', async () => {
1422
+ const method = new EVMSignMessageEIP712({
1423
+ id: 1,
1424
+ payload: {
1425
+ method: 'evmSignMessageEIP712',
1426
+ path: "m/44'/60'/0'/0/0",
1427
+ domainHash: '0x'.concat('11'.repeat(32)),
1428
+ messageHash: '0x'.concat('22'.repeat(32)),
1429
+ },
1430
+ });
1431
+
1432
+ method.init();
1433
+ (method as any).device = stubDevice({
1434
+ features: {
1435
+ onekey_device_type: 'pro2',
1436
+ },
1437
+ originalDescriptor: {
1438
+ protocolType: 'V2',
1439
+ },
1440
+ getCurrentFirmwareVersionString: () => '4.15.0',
1441
+ getCurrentMethodVersionRange: (selectRange: (deviceType: string) => unknown) =>
1442
+ selectRange('pro2'),
1443
+ getCurrentFirmwareType: () => 'universal',
1444
+ });
1445
+
1446
+ await expect(method.run()).rejects.toEqual(
1447
+ expect.objectContaining({
1448
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1449
+ })
1450
+ );
1451
+ });
1452
+
1453
+ test('returns a typed unsupported error for Tron sign message V1 before device binding', () => {
1454
+ const method = new TronSignMessage({
1455
+ id: 1,
1456
+ payload: {
1457
+ method: 'tronSignMessage',
1458
+ path: "m/44'/195'/0'/0/0",
1459
+ messageHex: '0x1234',
1460
+ messageType: 'V1',
1461
+ },
1462
+ });
1463
+
1464
+ expect(() => method.init()).toThrow(
1465
+ expect.objectContaining({
1466
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1467
+ })
1468
+ );
1469
+ });
1470
+
1471
+ test('does not mark Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1472
+ const features = {
1473
+ onekey_device_type: 'pro2',
1474
+ } as Features;
1475
+
1476
+ const tronMethod = new TronGetAddress({
1477
+ id: 1,
1478
+ payload: {
1479
+ method: 'tronGetAddress',
1480
+ path: "m/44'/195'/0'/0/0",
1481
+ showOnOneKey: false,
1482
+ },
1483
+ });
1484
+ const solMethod = new SolGetAddress({
1485
+ id: 2,
1486
+ payload: {
1487
+ method: 'solGetAddress',
1488
+ path: "m/44'/501'/0'/0'",
1489
+ showOnOneKey: false,
1490
+ },
1491
+ });
1492
+ const tonGetAddressMethod = new TonGetAddress({
1493
+ id: 3,
1494
+ payload: {
1495
+ method: 'tonGetAddress',
1496
+ path: "m/44'/607'/0'",
1497
+ showOnOneKey: false,
1498
+ },
1499
+ });
1500
+ const tonSignMessageMethod = new TonSignMessage({
1501
+ id: 4,
1502
+ payload: {
1503
+ method: 'tonSignMessage',
1504
+ path: "m/44'/607'/0'",
1505
+ },
1506
+ });
1507
+ const tonSignProofMethod = new TonSignProof({
1508
+ id: 5,
1509
+ payload: {
1510
+ method: 'tonSignProof',
1511
+ path: "m/44'/607'/0'",
1512
+ },
1513
+ });
1514
+ const tonSignDataMethod = new TonSignData({
1515
+ id: 6,
1516
+ payload: {
1517
+ method: 'tonSignData',
1518
+ path: "m/44'/607'/0'",
1519
+ },
1520
+ });
1521
+ const suiGetAddressMethod = new SuiGetAddress({
1522
+ id: 7,
1523
+ payload: {
1524
+ method: 'suiGetAddress',
1525
+ path: "m/44'/784'/0'/0'/0'",
1526
+ showOnOneKey: false,
1527
+ },
1528
+ });
1529
+ const suiGetPublicKeyMethod = new SuiGetPublicKey({
1530
+ id: 8,
1531
+ payload: {
1532
+ method: 'suiGetPublicKey',
1533
+ path: "m/44'/784'/0'/0'/0'",
1534
+ },
1535
+ });
1536
+ const suiSignMessageMethod = new SuiSignMessage({
1537
+ id: 9,
1538
+ payload: {
1539
+ method: 'suiSignMessage',
1540
+ path: "m/44'/784'/0'/0'/0'",
1541
+ messageHex: '0x1234',
1542
+ },
1543
+ });
1544
+ const suiSignTransactionMethod = new SuiSignTransaction({
1545
+ id: 10,
1546
+ payload: {
1547
+ method: 'suiSignTransaction',
1548
+ path: "m/44'/784'/0'/0'/0'",
1549
+ rawTx: '0x1234',
1550
+ },
1551
+ });
1552
+ const polkadotGetAddressMethod = new PolkadotGetAddress({
1553
+ id: 11,
1554
+ payload: {
1555
+ method: 'polkadotGetAddress',
1556
+ path: "m/44'/354'/0'/0'/0'",
1557
+ prefix: 0,
1558
+ network: 'polkadot',
1559
+ showOnOneKey: false,
1560
+ },
1561
+ });
1562
+
1563
+ polkadotGetAddressMethod.init();
1564
+
1565
+ expect(
1566
+ isMethodVersionRangeUnsupported(
1567
+ getMethodVersionRange(features, type => tronMethod.getVersionRange()[type])
1568
+ )
1569
+ ).toBe(false);
1570
+ expect(
1571
+ isMethodVersionRangeUnsupported(
1572
+ getMethodVersionRange(features, type => solMethod.getVersionRange()[type])
1573
+ )
1574
+ ).toBe(false);
1575
+ expect(
1576
+ getMethodVersionRange(features, type => tonGetAddressMethod.getVersionRange()[type])
1577
+ ).toEqual({
1578
+ min: '0.0.0',
1579
+ });
1580
+ expect(
1581
+ getMethodVersionRange(features, type => tonSignMessageMethod.getVersionRange()[type])
1582
+ ).toEqual({
1583
+ min: '0.0.0',
1584
+ });
1585
+ expect(
1586
+ getMethodVersionRange(features, type => tonSignProofMethod.getVersionRange()[type])
1587
+ ).toEqual({
1588
+ min: '0.0.0',
1589
+ });
1590
+ expect(
1591
+ getMethodVersionRange(features, type => tonSignDataMethod.getVersionRange()[type])
1592
+ ).toEqual({
1593
+ min: '0.0.0',
1594
+ });
1595
+ expect(
1596
+ getMethodVersionRange(features, type => suiGetAddressMethod.getVersionRange()[type])
1597
+ ).toEqual({
1598
+ min: '0.0.0',
1599
+ });
1600
+ expect(
1601
+ getMethodVersionRange(features, type => suiGetPublicKeyMethod.getVersionRange()[type])
1602
+ ).toEqual({
1603
+ min: '0.0.0',
1604
+ });
1605
+ expect(
1606
+ getMethodVersionRange(features, type => suiSignMessageMethod.getVersionRange()[type])
1607
+ ).toEqual({
1608
+ min: '0.0.0',
1609
+ });
1610
+ expect(
1611
+ getMethodVersionRange(features, type => suiSignTransactionMethod.getVersionRange()[type])
1612
+ ).toEqual({
1613
+ min: '0.0.0',
1614
+ });
1615
+ expect(
1616
+ getMethodVersionRange(features, type => polkadotGetAddressMethod.getVersionRange()[type])
1617
+ ).toEqual({
1618
+ min: '0.0.0',
1619
+ });
1620
+ });
1621
+
1622
+ test('allows Pro2 Solana signing methods through Protocol V2 version checks', () => {
1623
+ const features = {
1624
+ onekey_device_type: 'pro2',
1625
+ } as Features;
1626
+
1627
+ const solSignMessageMethod = new SolSignMessage({
1628
+ id: 1,
1629
+ payload: {
1630
+ method: 'solSignMessage',
1631
+ path: "m/44'/501'/0'/0'",
1632
+ messageHex: '48656c6c6f',
1633
+ },
1634
+ });
1635
+ const solSignOffchainMessageMethod = new SolSignOffchainMessage({
1636
+ id: 2,
1637
+ payload: {
1638
+ method: 'solSignOffchainMessage',
1639
+ path: "m/44'/501'/0'/0'",
1640
+ messageHex: '48656c6c6f',
1641
+ },
1642
+ });
1643
+ const solSignTransactionMethod = new SolSignTransaction({
1644
+ id: 3,
1645
+ payload: {
1646
+ method: 'solSignTransaction',
1647
+ path: "m/44'/501'/0'/0'",
1648
+ rawTx: '00',
1649
+ },
1650
+ });
1651
+ const solSignVersionedTransactionMethod = new SolSignTransaction({
1652
+ id: 4,
1653
+ payload: {
1654
+ method: 'solSignTransaction',
1655
+ path: "m/44'/501'/0'/0'",
1656
+ rawTx: '80',
1657
+ },
1658
+ });
1659
+
1660
+ solSignTransactionMethod.init();
1661
+ solSignVersionedTransactionMethod.init();
1662
+
1663
+ expect(
1664
+ getMethodVersionRange(features, type => solSignMessageMethod.getVersionRange()[type])
1665
+ ).toEqual({
1666
+ min: '0.0.0',
1667
+ });
1668
+ expect(
1669
+ getMethodVersionRange(features, type => solSignOffchainMessageMethod.getVersionRange()[type])
1670
+ ).toEqual({
1671
+ min: '0.0.0',
1672
+ });
1673
+ expect(
1674
+ getMethodVersionRange(features, type => solSignTransactionMethod.getVersionRange()[type])
1675
+ ).toEqual({
1676
+ min: '0.0.0',
1677
+ });
1678
+ expect(
1679
+ getMethodVersionRange(
1680
+ features,
1681
+ type => solSignVersionedTransactionMethod.getVersionRange()[type]
1682
+ )
1683
+ ).toEqual({
1684
+ min: '0.0.0',
1685
+ });
1686
+ });
1687
+
1688
+ test('includes TON signData in the Protocol V2 protobuf schema', () => {
1689
+ const protocolV2Messages = DataManager.getProtobufMessages('v2Schema') as any;
1690
+
1691
+ expect(protocolV2Messages.nested.TonSignData).toBeDefined();
1692
+ expect(protocolV2Messages.nested.TonSignedData).toBeDefined();
1693
+ expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignData).toBe(11908);
1694
+ expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignedData).toBe(11909);
1695
+ });
1696
+
1697
+ test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
1698
+ const rawTx = '0x'.concat('ab'.repeat(5000));
1699
+ const typedCall = jest.fn(() => ({
1700
+ type: 'SuiSignedTx',
1701
+ message: {
1702
+ public_key: '',
1703
+ signature: '',
1704
+ },
1705
+ }));
1706
+ const method = new SuiSignTransaction({
1707
+ id: 1,
1708
+ payload: {
1709
+ method: 'suiSignTransaction',
1710
+ path: "m/44'/784'/0'/0'/0'",
1711
+ rawTx,
1712
+ },
1713
+ });
1714
+
1715
+ method.init();
1716
+ (method as any).device = stubDevice({
1717
+ features: {
1718
+ onekey_device_type: 'pro2',
1719
+ },
1720
+ originalDescriptor: {
1721
+ protocolType: 'V2',
1722
+ },
1723
+ getCommands: () => ({
1724
+ typedCall,
1725
+ }),
1726
+ });
1727
+
1728
+ await method.run();
1729
+
1730
+ expect(typedCall).toHaveBeenCalledTimes(1);
1731
+ const [, , params] = typedCall.mock.calls[0];
1732
+ expect(params).toEqual(
1733
+ expect.objectContaining({
1734
+ raw_tx: '',
1735
+ data_length: 5000,
1736
+ })
1737
+ );
1738
+ expect(params.data_initial_chunk).toHaveLength(2048);
1739
+ });
1740
+
1741
+ test('accepts string XRP payment amount values', () => {
1742
+ const method = new XrpSignTransaction({
1743
+ id: 1,
1744
+ payload: {
1745
+ method: 'xrpSignTransaction',
1746
+ path: "m/44'/144'/0'/0/0",
1747
+ transaction: {
1748
+ fee: '100000',
1749
+ flags: 2147483648,
1750
+ sequence: 25,
1751
+ maxLedgerVersion: 8820051,
1752
+ payment: {
1753
+ amount: '100000000',
1754
+ destination: 'rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws',
1755
+ },
1756
+ },
1757
+ },
1758
+ });
1759
+
1760
+ expect(() => method.init()).not.toThrow();
1761
+ expect(method.params.payment?.amount).toBe('100000000');
1762
+ });
1763
+
1764
+ test('accepts Conflux base32 recipient addresses without hex formatting them', () => {
1765
+ const to = 'cfx:aak2rra2njvd77ezwjvx04kkds9fzagfe6ku8scz91';
1766
+ const method = new ConfluxSignTransaction({
1767
+ id: 1,
1768
+ payload: {
1769
+ method: 'confluxSignTransaction',
1770
+ path: "m/44'/503'/0'/0/0",
1771
+ transaction: {
1772
+ to,
1773
+ value: '0x0',
1774
+ data: '0x',
1775
+ chainId: 1,
1776
+ nonce: '0x00',
1777
+ epochHeight: '0x00',
1778
+ gasLimit: '0x5208',
1779
+ storageLimit: '0x5208',
1780
+ gasPrice: '0xbebc200',
1781
+ },
1782
+ },
1783
+ });
1784
+
1785
+ expect(() => method.init()).not.toThrow();
1786
+ expect(method.formattedTx?.to).toBe(to);
1787
+ });
1788
+
1789
+ test('returns a typed unsupported error for Dynex signing on Protocol V2', async () => {
1790
+ const method = new DnxSignTransaction({
1791
+ id: 1,
1792
+ payload: {
1793
+ method: 'dnxSignTransaction',
1794
+ path: "m/44'/29538'/0'/0/0",
1795
+ inputs: [
1796
+ {
1797
+ prevIndex: 1,
1798
+ globalIndex: 1,
1799
+ txPubkey: '00',
1800
+ prevOutPubkey: '00',
1801
+ amount: '1',
1802
+ },
1803
+ ],
1804
+ toAddress: 'dnx-address',
1805
+ amount: '1',
1806
+ fee: '1',
1807
+ },
1808
+ });
1809
+
1810
+ method.init();
1811
+ (method as any).device = stubDevice({
1812
+ originalDescriptor: { protocolType: 'V2' },
1813
+ features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1814
+ });
1815
+
1816
+ await expect(method.run()).rejects.toMatchObject({
1817
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1818
+ });
1819
+ });
1820
+
1821
+ test('returns a typed unsupported error for Dynex address on Protocol V2', async () => {
1822
+ const method = new DnxGetAddress({
1823
+ id: 1,
1824
+ payload: {
1825
+ method: 'dnxGetAddress',
1826
+ path: "m/44'/29538'/0'/0/0",
1827
+ showOnOneKey: false,
1828
+ },
1829
+ });
1830
+
1831
+ method.init();
1832
+ (method as any).device = stubDevice({
1833
+ originalDescriptor: { protocolType: 'V2' },
1834
+ features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
1835
+ });
1836
+
1837
+ await expect(method.run()).rejects.toMatchObject({
1838
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1839
+ });
1840
+ });
1841
+ });
1842
+
1843
+ describe('Protocol V2 firmware update targets', () => {
1844
+ test('keeps Protocol V2 firmware updates off the legacy firmwareUpdateV3 path', async () => {
1845
+ const method = new FirmwareUpdateV3({
1846
+ id: 1,
1847
+ payload: {
1848
+ method: 'firmwareUpdateV3',
1849
+ },
1850
+ });
1851
+ (method as any).device = stubDevice({
1852
+ originalDescriptor: { protocolType: 'V2' },
1853
+ });
1854
+
1855
+ await expect(method.run()).rejects.toThrow('firmwareUpdateV4');
1856
+ });
1857
+
1858
+ test('uses Protocol V2 features after BLE final reconnect without legacy Initialize', async () => {
1859
+ const method = new FirmwareUpdateV4({
1860
+ id: 1,
1861
+ payload: {
1862
+ method: 'firmwareUpdateV4',
1863
+ },
1864
+ });
1865
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' });
1866
+ const typedCall = jest.fn().mockImplementation((name: string) => {
1867
+ if (name === 'DevGetDeviceInfo') {
1868
+ return Promise.resolve({
1869
+ type: 'DeviceInfo',
1870
+ message: {
1871
+ fw: {
1872
+ boot: { version: '0.0.0' },
1873
+ app: { version: '0.0.0' },
1874
+ },
1875
+ bt: {
1876
+ app: { version: '0.0.0' },
1877
+ },
1878
+ },
1879
+ });
1880
+ }
1881
+ return Promise.reject(new Error(`unexpected call ${name}`));
1882
+ });
1883
+ const commands = { typedCall };
1884
+
1885
+ (method as any).isBleReconnect = jest.fn(() => true);
1886
+ (method as any).device = stubDevice({
1887
+ originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1888
+ deviceConnector: { acquire },
1889
+ getCommands: () => commands,
1890
+ _updateFeatures: jest.fn(),
1891
+ });
1892
+
1893
+ const versions = await (method as any).waitForProtocolV2FinalFeatures();
1894
+
1895
+ expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2');
1896
+ // 更新完成判定使用 VERSIONS 请求(含 SE targets),scope 与请求内容一致
1897
+ expect(typedCall).toHaveBeenNthCalledWith(
1898
+ 1,
1899
+ 'DevGetDeviceInfo',
1900
+ 'DeviceInfo',
1901
+ {
1902
+ targets: {
1903
+ hw: true,
1904
+ fw: true,
1905
+ bt: true,
1906
+ se1: true,
1907
+ se2: true,
1908
+ se3: true,
1909
+ se4: true,
1910
+ status: true,
1911
+ },
1912
+ types: {
1913
+ version: true,
1914
+ specific: true,
1915
+ },
1916
+ },
1917
+ { timeoutMs: 5000 }
1918
+ );
1919
+ expect(typedCall).toHaveBeenCalledTimes(1);
1920
+ expect(typedCall).not.toHaveBeenCalledWith('Initialize', 'Features', {});
1921
+ expect(versions).toEqual({
1922
+ bootloaderVersion: '0.0.0',
1923
+ bleVersion: '0.0.0',
1924
+ firmwareVersion: '0.0.0',
1925
+ });
1926
+ });
1927
+
1928
+ test('skips automatic Protocol V2 bootloader entry before upload while boot flow is disabled', async () => {
1929
+ const method = new FirmwareUpdateV4({
1930
+ id: 1,
1931
+ payload: {
1932
+ method: 'firmwareUpdateV4',
1933
+ },
1934
+ });
1935
+ method.init();
1936
+
1937
+ (method as any).device = stubDevice({
1938
+ originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1939
+ features: { capabilities: [] },
1940
+ });
1941
+ (method as any).prepareResourceBinary = jest.fn().mockResolvedValue(null);
1942
+ (method as any).prepareFirmwareAndBleBinary = jest.fn().mockResolvedValue([
1943
+ {
1944
+ fileName: 'ble-firmware.bin',
1945
+ binary: new Uint8Array([1, 2, 3]).buffer,
1946
+ },
1947
+ ]);
1948
+ (method as any).prepareBootloaderBinary = jest.fn().mockResolvedValue(null);
1949
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
1950
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
1951
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
1952
+ bootloaderVersion: '0.2.0',
1953
+ bleVersion: '4.5.6',
1954
+ firmwareVersion: '1.2.3',
1955
+ });
1956
+ (method as any).protocolV2Reboot = jest.fn();
1957
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
1958
+ method.postTipMessage = jest.fn();
1959
+
1960
+ await method.run();
1961
+
1962
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
1963
+ resourceBinary: null,
1964
+ fwBinaryMap: [
1965
+ {
1966
+ fileName: 'ble-firmware.bin',
1967
+ binary: expect.any(ArrayBuffer),
1968
+ },
1969
+ ],
1970
+ bootloaderBinary: null,
1971
+ });
1972
+ expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
1973
+ });
1974
+
1975
+ test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
1976
+ const method = new FirmwareUpdateV4({
1977
+ id: 1,
1978
+ payload: {
1979
+ method: 'firmwareUpdateV4',
1980
+ },
1981
+ });
1982
+ const acquire = jest.fn().mockResolvedValue(undefined);
1983
+ (method as any).device = stubDevice({
1984
+ originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
1985
+ features: { bootloader_mode: false, capabilities: [] },
1986
+ isBootloader: () => false,
1987
+ acquire,
1988
+ });
1989
+ (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
1990
+ message: 'Device rebooted successfully',
1991
+ });
1992
+ (method as any).checkDeviceToBootloader = jest.fn(() => {
1993
+ (method as any).checkPromise = { promise: Promise.resolve(true) };
1994
+ });
1995
+ method.postTipMessage = jest.fn();
1996
+
1997
+ await (method as any).enterProtocolV2BootloaderMode();
1998
+
1999
+ expect(method.postTipMessage).toHaveBeenCalledWith('AutoRebootToBootloader');
2000
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Bootloader);
2001
+ expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
2002
+ expect((method as any).checkDeviceToBootloader).toHaveBeenCalledWith(undefined);
2003
+ expect(acquire).toHaveBeenCalledTimes(1);
2004
+ });
2005
+
2006
+ test('reboots Protocol V2 firmware flow back to normal before final feature polling', async () => {
2007
+ const method = new FirmwareUpdateV4({
2008
+ id: 1,
2009
+ payload: {
2010
+ method: 'firmwareUpdateV4',
2011
+ },
2012
+ });
2013
+ method.postTipMessage = jest.fn();
2014
+ (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
2015
+ message: 'Device rebooted successfully',
2016
+ });
2017
+
2018
+ await (method as any).exitProtocolV2BootloaderToNormal();
2019
+
2020
+ expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
2021
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Normal);
2022
+ });
2023
+
2024
+ test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
2025
+ const method = new FirmwareUpdateV4({
2026
+ id: 1,
2027
+ payload: {
2028
+ method: 'firmwareUpdateV4',
2029
+ },
2030
+ });
2031
+ const typedCall = jest
2032
+ .fn()
2033
+ .mockRejectedValue(
2034
+ new Error("The operation couldn't be completed. (MultiplatformBleAdapter.RxError error 6.)")
2035
+ );
2036
+
2037
+ (method as any).device = stubDevice({
2038
+ getCommands: () => ({ typedCall }),
2039
+ });
2040
+
2041
+ await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2042
+ message: 'Device rebooted successfully',
2043
+ });
2044
+ });
2045
+
2046
+ test('treats direct disconnect during Protocol V2 normal reboot as expected', async () => {
2047
+ const method = new FirmwareUpdateV4({
2048
+ id: 1,
2049
+ payload: {
2050
+ method: 'firmwareUpdateV4',
2051
+ },
2052
+ });
2053
+ const typedCall = jest
2054
+ .fn()
2055
+ .mockRejectedValue(new Error('Connection error has occured: Device disconnected'));
2056
+
2057
+ (method as any).device = stubDevice({
2058
+ getCommands: () => ({ typedCall }),
2059
+ });
2060
+
2061
+ await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2062
+ message: 'Device rebooted successfully',
2063
+ });
2064
+ });
2065
+
2066
+ test('treats WebUSB open NotFoundError during Protocol V2 firmware update as expected disconnect', async () => {
2067
+ const method = new FirmwareUpdateV4({
2068
+ id: 1,
2069
+ payload: {
2070
+ method: 'firmwareUpdateV4',
2071
+ },
2072
+ });
2073
+ const typedCall = jest
2074
+ .fn()
2075
+ .mockRejectedValue(
2076
+ new DOMException(
2077
+ "Failed to execute 'open' on 'USBDevice': The device was disconnected",
2078
+ 'NotFoundError'
2079
+ )
2080
+ );
2081
+
2082
+ (method as any).device = stubDevice({
2083
+ getCommands: () => ({ typedCall }),
2084
+ });
2085
+ method.postTipMessage = jest.fn();
2086
+
2087
+ await expect(
2088
+ (method as any).protocolV2StartFirmwareUpdate({
2089
+ targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2090
+ })
2091
+ ).resolves.toBeUndefined();
2092
+
2093
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2094
+ });
2095
+
2096
+ test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => {
2097
+ const method = new FirmwareUpdateV4({
2098
+ id: 1,
2099
+ payload: {
2100
+ method: 'firmwareUpdateV4',
2101
+ },
2102
+ });
2103
+ const typedCall = jest
2104
+ .fn()
2105
+ .mockRejectedValueOnce(
2106
+ new Error(
2107
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2108
+ )
2109
+ )
2110
+ .mockResolvedValueOnce({
2111
+ type: 'DevFirmwareUpdateStatus',
2112
+ message: {
2113
+ targets: [{ target_id: 2, status: 0 }],
2114
+ },
2115
+ });
2116
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2117
+
2118
+ (method as any).device = stubDevice({
2119
+ getCommands: () => ({ typedCall }),
2120
+ });
2121
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2122
+ method.postProgressMessage = jest.fn();
2123
+
2124
+ await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2125
+ { target_id: 2, path: 'vol1:ble-firmware.bin' },
2126
+ ]);
2127
+
2128
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2129
+ expect(typedCall).toHaveBeenCalledTimes(2);
2130
+ });
2131
+
2132
+ test('uses dev firmware status semantics for Protocol V2 install polling', () => {
2133
+ const method = new FirmwareUpdateV4({
2134
+ id: 1,
2135
+ payload: {
2136
+ method: 'firmwareUpdateV4',
2137
+ },
2138
+ });
2139
+ method.postProgressMessage = jest.fn();
2140
+
2141
+ const expectedTargetIds = new Set([3]);
2142
+
2143
+ expect(
2144
+ (method as any).assertProtocolV2TargetStatus(
2145
+ [{ target_id: 3, status: 0 }],
2146
+ expectedTargetIds
2147
+ )
2148
+ ).toBe(true);
2149
+
2150
+ expect(
2151
+ (method as any).assertProtocolV2TargetStatus(
2152
+ [{ target_id: 3, status: 1 }],
2153
+ expectedTargetIds
2154
+ )
2155
+ ).toBe(false);
2156
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2157
+
2158
+ try {
2159
+ (method as any).assertProtocolV2TargetStatus(
2160
+ [{ target_id: 3, status: 2 }],
2161
+ expectedTargetIds
2162
+ );
2163
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2164
+ } catch (error: any) {
2165
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2166
+ }
2167
+ });
2168
+
2169
+ test('passes resource, bootloader, BLE, SE and app files to DeviceFirmwareUpdate targets', async () => {
2170
+ const method = new FirmwareUpdateV4({
2171
+ id: 1,
2172
+ payload: {
2173
+ method: 'firmwareUpdateV4',
2174
+ },
2175
+ });
2176
+
2177
+ const writtenPaths: string[] = [];
2178
+ method.postTipMessage = jest.fn();
2179
+ method.postProgressMessage = jest.fn();
2180
+ (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => {
2181
+ writtenPaths.push(params.filePath);
2182
+ return Number(params.processedSize ?? 0) + Number(params.payload.byteLength);
2183
+ });
2184
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2185
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2186
+ .fn()
2187
+ .mockResolvedValue(undefined);
2188
+
2189
+ await (method as any).executeProtocolV2Update({
2190
+ // resource 只支持单文件 .bin,整文件一次上传
2191
+ resourceBinary: new Uint8Array([1, 2, 3]).buffer,
2192
+ bootloaderBinary: new Uint8Array([4, 5]).buffer,
2193
+ fwBinaryMap: [
2194
+ {
2195
+ fileName: 'ble-firmware.bin',
2196
+ binary: new Uint8Array([6]).buffer,
2197
+ },
2198
+ {
2199
+ fileName: 'se1-firmware.bin',
2200
+ binary: new Uint8Array([7]).buffer,
2201
+ },
2202
+ {
2203
+ fileName: 'firmware.bin',
2204
+ binary: new Uint8Array([8]).buffer,
2205
+ },
2206
+ ],
2207
+ });
2208
+
2209
+ expect(writtenPaths).toEqual([
2210
+ 'vol1:resource.bin',
2211
+ 'vol1:bootloader.bin',
2212
+ 'vol1:ble-firmware.bin',
2213
+ 'vol1:se1-firmware.bin',
2214
+ 'vol1:firmware.bin',
2215
+ ]);
2216
+ // DevFirmwareTargetType:RESOURCE=10, MAIN_BOOT=1, BT=2, SE1=3, MAIN_APP=0
2217
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2218
+ targets: [
2219
+ { target_id: 10, path: 'vol1:resource.bin' },
2220
+ { target_id: 1, path: 'vol1:bootloader.bin' },
2221
+ { target_id: 2, path: 'vol1:ble-firmware.bin' },
2222
+ { target_id: 3, path: 'vol1:se1-firmware.bin' },
2223
+ { target_id: 0, path: 'vol1:firmware.bin' },
2224
+ ],
2225
+ });
2226
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
2227
+ expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled();
2228
+ });
2229
+
2230
+ test('passes explicit per-target binaries through without file name heuristics', async () => {
2231
+ const method = new FirmwareUpdateV4({
2232
+ id: 1,
2233
+ payload: {
2234
+ method: 'firmwareUpdateV4',
2235
+ platform: 'web',
2236
+ romloaderBinary: new Uint8Array([1]).buffer,
2237
+ applicationP2Binary: new Uint8Array([2]).buffer,
2238
+ se04Binary: new Uint8Array([3]).buffer,
2239
+ },
2240
+ });
2241
+ method.init();
2242
+
2243
+ const explicit = (method as any).collectExplicitTargetBinaries();
2244
+ expect(explicit).toEqual([
2245
+ { fileName: 'romloader.bin', binary: expect.anything(), targetId: 1 },
2246
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 0 },
2247
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 6 },
2248
+ ]);
2249
+
2250
+ method.postTipMessage = jest.fn();
2251
+ method.postProgressMessage = jest.fn();
2252
+ (method as any).protocolV2CommonUpdateProcess = jest
2253
+ .fn()
2254
+ .mockImplementation(params =>
2255
+ Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength))
2256
+ );
2257
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined);
2258
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2259
+ .fn()
2260
+ .mockResolvedValue(undefined);
2261
+
2262
+ await (method as any).executeProtocolV2Update({
2263
+ resourceBinary: null,
2264
+ bootloaderBinary: null,
2265
+ fwBinaryMap: explicit,
2266
+ });
2267
+
2268
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2269
+ targets: [
2270
+ { target_id: 1, path: 'vol1:romloader.bin' },
2271
+ { target_id: 0, path: 'vol1:application_p2.bin' },
2272
+ { target_id: 6, path: 'vol1:se04.bin' },
2273
+ ],
2274
+ });
2275
+ });
2276
+
2277
+ test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
2278
+ const method = new FirmwareUpdateV4({
2279
+ id: 1,
2280
+ payload: {
2281
+ method: 'firmwareUpdateV4',
2282
+ },
2283
+ });
2284
+ const typedCall = jest.fn(
2285
+ (
2286
+ _name: string,
2287
+ _resType: string,
2288
+ params: { file: { offset: number; data: { byteLength: number } } }
2289
+ ) =>
2290
+ Promise.resolve({
2291
+ type: 'FilesystemFile',
2292
+ message: {
2293
+ processed_byte: params.file.offset + params.file.data.byteLength,
2294
+ },
2295
+ })
2296
+ );
2297
+
2298
+ (method as any).device = stubDevice({
2299
+ getCommands: () => ({ typedCall }),
2300
+ });
2301
+ method.postProgressMessage = jest.fn();
2302
+
2303
+ await (method as any).protocolV2CommonUpdateProcess({
2304
+ payload: new Uint8Array(4097).buffer,
2305
+ filePath: 'vol1:firmware.bin',
2306
+ processedSize: 0,
2307
+ totalSize: 4097,
2308
+ });
2309
+
2310
+ const writePayloads = typedCall.mock.calls.map(call => call[2]);
2311
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4096]);
2312
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4096, 1]);
2313
+ expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
2314
+ expect(writePayloads.every(payload => payload.append === false)).toBe(true);
2315
+ });
2316
+
2317
+ test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
2318
+ const method = new FirmwareUpdateV4({
2319
+ id: 1,
2320
+ payload: {
2321
+ method: 'firmwareUpdateV4',
2322
+ },
2323
+ });
2324
+ const typedCall = jest.fn((name: string, _resType: string | string[], params: any) => {
2325
+ if (name === 'FilesystemFileWrite') {
2326
+ return Promise.resolve({
2327
+ type: 'FilesystemFile',
2328
+ message: {
2329
+ processed_byte: params.file.data.byteLength,
2330
+ },
2331
+ });
2332
+ }
2333
+ if (name === 'DevFirmwareUpdate') {
2334
+ return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
2335
+ }
2336
+ return Promise.reject(new Error(`unexpected call ${name}`));
2337
+ });
2338
+
2339
+ (method as any).device = stubDevice({
2340
+ getCommands: () => ({ typedCall }),
2341
+ });
2342
+ method.postProgressMessage = jest.fn();
2343
+ method.postTipMessage = jest.fn();
2344
+
2345
+ await (method as any).executeProtocolV2Update({
2346
+ resourceBinary: null,
2347
+ bootloaderBinary: null,
2348
+ fwBinaryMap: [
2349
+ {
2350
+ fileName: 'firmware.bin',
2351
+ binary: new Uint8Array(4097).buffer,
2352
+ },
2353
+ ],
2354
+ });
2355
+
2356
+ expect(typedCall).toHaveBeenCalledWith(
2357
+ 'DevFirmwareUpdate',
2358
+ ['Success', 'DevFirmwareUpdateStatus'],
2359
+ {
2360
+ targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2361
+ },
2362
+ expect.any(Object)
2363
+ );
2364
+ });
2365
+
2366
+ test('caps native BLE firmware upload chunks below the WebUSB limit', async () => {
2367
+ const method = new FirmwareUpdateV4({
2368
+ id: 1,
2369
+ payload: {
2370
+ method: 'firmwareUpdateV4',
2371
+ },
2372
+ });
2373
+ const typedCall = jest.fn(
2374
+ (
2375
+ _name: string,
2376
+ _resType: string,
2377
+ params: { file: { offset: number; data: { byteLength: number } } }
2378
+ ) =>
2379
+ Promise.resolve({
2380
+ type: 'FilesystemFile',
2381
+ message: {
2382
+ processed_byte: params.file.offset + params.file.data.byteLength,
2383
+ },
2384
+ })
2385
+ );
2386
+
2387
+ (method as any).params = {
2388
+ platform: 'native',
2389
+ chunkSize: 4096,
2390
+ };
2391
+ (method as any).device = stubDevice({
2392
+ getCommands: () => ({ typedCall }),
2393
+ });
2394
+ method.postProgressMessage = jest.fn();
2395
+
2396
+ await (method as any).protocolV2CommonUpdateProcess({
2397
+ payload: new Uint8Array(1801).buffer,
2398
+ filePath: 'vol1:ble-firmware.bin',
2399
+ processedSize: 0,
2400
+ totalSize: 1801,
2401
+ });
2402
+
2403
+ const writePayloads = typedCall.mock.calls.map(call => call[2]);
2404
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
2405
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
2406
+ });
2407
+
2408
+ test('consumes Protocol V2 install progress before final update success', async () => {
2409
+ const method = new FirmwareUpdateV4({
2410
+ id: 1,
2411
+ payload: {
2412
+ method: 'firmwareUpdateV4',
2413
+ },
2414
+ });
2415
+ const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: { message: 'ok' } });
2416
+
2417
+ (method as any).device = stubDevice({
2418
+ getCommands: () => ({ typedCall }),
2419
+ });
2420
+ method.postProgressMessage = jest.fn();
2421
+ method.postTipMessage = jest.fn();
2422
+
2423
+ await (method as any).protocolV2StartFirmwareUpdate({
2424
+ targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2425
+ });
2426
+
2427
+ const callOptions = typedCall.mock.calls[0][3];
2428
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2429
+ expect(callOptions.intermediateTypes).toEqual(['DevFirmwareInstallProgress']);
2430
+ callOptions.onIntermediateResponse({
2431
+ type: 'DevFirmwareInstallProgress',
2432
+ message: { target_id: 0, progress: 42 },
2433
+ });
2434
+
2435
+ expect(method.postProgressMessage).toHaveBeenCalledWith(42, 'installingFirmware');
2436
+ });
2437
+
2438
+ test('accepts Protocol V2 firmware update status as start response', async () => {
2439
+ const method = new FirmwareUpdateV4({
2440
+ id: 1,
2441
+ payload: {
2442
+ method: 'firmwareUpdateV4',
2443
+ },
2444
+ });
2445
+ const typedCall = jest.fn().mockResolvedValue({
2446
+ type: 'DevFirmwareUpdateStatus',
2447
+ message: { targets: [{ target_id: 0, status: 1 }] },
2448
+ });
2449
+
2450
+ (method as any).device = stubDevice({
2451
+ getCommands: () => ({ typedCall }),
2452
+ });
2453
+ method.postProgressMessage = jest.fn();
2454
+ method.postTipMessage = jest.fn();
2455
+
2456
+ await (method as any).protocolV2StartFirmwareUpdate({
2457
+ targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2458
+ });
2459
+
2460
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2461
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2462
+ });
2463
+ });
2464
+
2465
+ describe('Protocol V2 firmware update method', () => {
2466
+ test('returns DevFirmwareUpdateStatus from low-level update trigger', async () => {
2467
+ const method = new DeviceFirmwareUpdate({
2468
+ id: 1,
2469
+ payload: {
2470
+ method: 'deviceFirmwareUpdate',
2471
+ targetId: 0,
2472
+ path: 'vol0:firmware.bin',
2473
+ },
2474
+ });
2475
+ method.init();
2476
+
2477
+ const typedCall = jest.fn().mockResolvedValue({
2478
+ type: 'DevFirmwareUpdateStatus',
2479
+ message: { targets: [{ target_id: 0, status: 1 }] },
2480
+ });
2481
+
2482
+ (method as any).device = stubDevice({
2483
+ commands: { typedCall },
2484
+ });
2485
+
2486
+ await expect(method.run()).resolves.toEqual({
2487
+ targets: [{ target_id: 0, status: 1 }],
2488
+ });
2489
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2490
+ expect(typedCall.mock.calls[0][2]).toEqual({
2491
+ targets: [{ target_id: 0, path: 'vol0:firmware.bin' }],
2492
+ });
2493
+ });
2494
+
2495
+ test('treats WebUSB open NotFoundError from low-level update trigger as expected disconnect', async () => {
2496
+ const method = new DeviceFirmwareUpdate({
2497
+ id: 1,
2498
+ payload: {
2499
+ method: 'deviceFirmwareUpdate',
2500
+ targetId: 0,
2501
+ path: 'vol0:firmware.bin',
2502
+ },
2503
+ });
2504
+ method.init();
2505
+
2506
+ const typedCall = jest
2507
+ .fn()
2508
+ .mockRejectedValue(
2509
+ new DOMException(
2510
+ "Failed to execute 'open' on 'USBDevice': The device was disconnected",
2511
+ 'NotFoundError'
2512
+ )
2513
+ );
2514
+
2515
+ (method as any).device = stubDevice({
2516
+ commands: { typedCall },
2517
+ });
2518
+
2519
+ await expect(method.run()).resolves.toEqual({
2520
+ message: 'Device firmware update started',
2521
+ });
2522
+ });
2523
+
2524
+ test('rejects missing or invalid firmware targets before transport call', async () => {
2525
+ const typedCall = jest.fn();
2526
+ const method = new DeviceFirmwareUpdate({
2527
+ id: 1,
2528
+ payload: {
2529
+ method: 'deviceFirmwareUpdate',
2530
+ target_id: -1,
2531
+ path: 'vol0:firmware.bin',
2532
+ },
2533
+ });
2534
+ method.init();
2535
+ (method as any).device = stubDevice({
2536
+ commands: { typedCall },
2537
+ });
2538
+
2539
+ await expect(method.run()).rejects.toMatchObject({
2540
+ errorCode: HardwareErrorCode.CallMethodInvalidParameter,
2541
+ });
2542
+ expect(typedCall).not.toHaveBeenCalled();
2543
+ });
2544
+
2545
+ test('accepts targetId alias inside firmware targets', async () => {
2546
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
2547
+ const method = new DeviceFirmwareUpdate({
2548
+ id: 1,
2549
+ payload: {
2550
+ method: 'deviceFirmwareUpdate',
2551
+ targets: [
2552
+ {
2553
+ target_id: undefined,
2554
+ targetId: 3,
2555
+ path: 'vol0:firmware.bin',
2556
+ },
2557
+ ],
2558
+ } as any,
2559
+ });
2560
+ method.init();
2561
+ (method as any).device = stubDevice({
2562
+ commands: { typedCall },
2563
+ });
2564
+
2565
+ await method.run();
2566
+ expect(typedCall.mock.calls[0][2]).toEqual({
2567
+ targets: [{ target_id: 3, path: 'vol0:firmware.bin' }],
2568
+ });
2569
+ });
2570
+ });
2571
+
2572
+ describe('Protocol V2 reboot methods', () => {
2573
+ test('sends DevReboot from devReboot', async () => {
2574
+ const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2575
+ const method = new DevReboot({
2576
+ id: 1,
2577
+ payload: {
2578
+ method: 'devReboot',
2579
+ rebootType: 2,
2580
+ },
2581
+ });
2582
+ method.init();
2583
+ (method as any).device = stubDevice({ commands: { typedCall } });
2584
+
2585
+ await method.run();
2586
+
2587
+ expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
2588
+ reboot_type: 2,
2589
+ });
2590
+ });
2591
+
2592
+ test('sends DeviceReboot from deviceReboot', async () => {
2593
+ const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2594
+ const method = new DeviceReboot({
2595
+ id: 1,
2596
+ payload: {
2597
+ method: 'deviceReboot',
2598
+ rebootType: 2,
2599
+ },
2600
+ });
2601
+ method.init();
2602
+ (method as any).device = stubDevice({ commands: { typedCall } });
2603
+
2604
+ await method.run();
2605
+
2606
+ expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
2607
+ reboot_type: 2,
2608
+ });
2609
+ });
2610
+ });
2611
+
2612
+ describe('Protocol V2 raw device info method', () => {
2613
+ const buildMethod = (payload: Record<string, unknown> = {}) => {
2614
+ const method = new DeviceGetDeviceInfo({
2615
+ id: 1,
2616
+ payload: {
2617
+ method: 'deviceGetDeviceInfo',
2618
+ ...payload,
2619
+ },
2620
+ });
2621
+ method.init();
2622
+ return method;
2623
+ };
2624
+
2625
+ test('passes requested targets/types through and returns the raw DeviceInfo message', async () => {
2626
+ const method = buildMethod({
2627
+ targets: { hw: true, se1: true, junk: true },
2628
+ types: { version: true, hash: true },
2629
+ });
2630
+ const typedCall = jest.fn().mockResolvedValue({
2631
+ type: 'DeviceInfo',
2632
+ message: { protocol_version: 1, hw: { serial_no: 'PR2SERIAL' } },
2633
+ });
2634
+ (method as any).device = stubDevice({
2635
+ originalDescriptor: { protocolType: 'V2' },
2636
+ commands: { typedCall },
2637
+ });
2638
+
2639
+ await expect(method.run()).resolves.toEqual({
2640
+ protocol_version: 1,
2641
+ hw: { serial_no: 'PR2SERIAL' },
2642
+ });
2643
+ // 未知 key(junk)被过滤,已选 key 原样透传,不构建 DeviceProfile
2644
+ expect(typedCall).toHaveBeenCalledWith(
2645
+ 'DevGetDeviceInfo',
2646
+ 'DeviceInfo',
2647
+ {
2648
+ targets: { hw: true, se1: true },
2649
+ types: { version: true, hash: true },
2650
+ },
2651
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
2652
+ );
2653
+ });
2654
+
2655
+ test('defaults to the basic targets/types when none are given', async () => {
2656
+ const method = buildMethod();
2657
+ const typedCall = jest.fn().mockResolvedValue({ type: 'DeviceInfo', message: {} });
2658
+ (method as any).device = stubDevice({
2659
+ originalDescriptor: { protocolType: 'V2' },
2660
+ commands: { typedCall },
2661
+ });
2662
+
2663
+ await method.run();
2664
+
2665
+ expect(typedCall).toHaveBeenCalledWith(
2666
+ 'DevGetDeviceInfo',
2667
+ 'DeviceInfo',
2668
+ {
2669
+ targets: { hw: true, fw: true, bt: true, status: true },
2670
+ types: { version: true, specific: true },
2671
+ },
2672
+ { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
2673
+ );
2674
+ });
2675
+
2676
+ test('rejects on Protocol V1 devices instead of sending an unknown message', async () => {
2677
+ const method = buildMethod();
2678
+ const typedCall = jest.fn();
2679
+ (method as any).device = stubDevice({
2680
+ originalDescriptor: { protocolType: 'V1' },
2681
+ commands: { typedCall },
2682
+ });
2683
+
2684
+ await expect(method.run()).rejects.toThrow();
2685
+ expect(typedCall).not.toHaveBeenCalled();
2686
+ });
2687
+ });
2688
+
2689
+ describe('Protocol V2 onboarding status method', () => {
2690
+ test('returns OnboardingStatus from low-level status query', async () => {
2691
+ const method = new DeviceGetOnboardingStatus({
2692
+ id: 1,
2693
+ payload: {
2694
+ method: 'deviceGetOnboardingStatus',
2695
+ },
2696
+ });
2697
+ method.init();
2698
+
2699
+ const typedCall = jest.fn().mockResolvedValue({
2700
+ type: 'OnboardingStatus',
2701
+ message: {
2702
+ step: 3,
2703
+ setup: {
2704
+ restore: {
2705
+ mnemonic: true,
2706
+ },
2707
+ },
2708
+ detail_code: 7,
2709
+ detail_str: 'Recovery Phrase',
2710
+ },
2711
+ });
2712
+
2713
+ (method as any).device = stubDevice({
2714
+ commands: { typedCall },
2715
+ });
2716
+
2717
+ await expect(method.run()).resolves.toEqual({
2718
+ step: 3,
2719
+ setup: {
2720
+ restore: {
2721
+ mnemonic: true,
2722
+ },
2723
+ },
2724
+ detail_code: 7,
2725
+ detail_str: 'Recovery Phrase',
2726
+ });
2727
+ expect(typedCall).toHaveBeenCalledWith('GetOnboardingStatus', 'OnboardingStatus', {});
2728
+ });
2729
+ });
2730
+
2731
+ describe('Protocol V2 file write method', () => {
2732
+ test('rejects invalid write parameters before transport call', () => {
2733
+ expect(() => {
2734
+ const method = new FileWrite({
2735
+ id: 1,
2736
+ payload: {
2737
+ method: 'fileWrite',
2738
+ path: 'vol1:test.bin',
2739
+ offset: -1,
2740
+ data: new Uint8Array([1]),
2741
+ },
2742
+ });
2743
+ method.init();
2744
+ }).toThrow(
2745
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
2746
+ );
2747
+
2748
+ expect(() => {
2749
+ const method = new FileWrite({
2750
+ id: 1,
2751
+ payload: {
2752
+ method: 'fileWrite',
2753
+ path: 'vol1:test.bin',
2754
+ } as any,
2755
+ });
2756
+ method.init();
2757
+ }).toThrow(
2758
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
2759
+ );
2760
+ });
2761
+
2762
+ test('uses demo-aligned overwrite and append defaults', async () => {
2763
+ const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 1 } });
2764
+ const method = new FileWrite({
2765
+ id: 1,
2766
+ payload: {
2767
+ method: 'fileWrite',
2768
+ path: 'vol1:test.bin',
2769
+ offset: 1,
2770
+ totalSize: 2,
2771
+ data: new Uint8Array([1]),
2772
+ },
2773
+ });
2774
+ (method as any).device = stubDevice({ commands: { typedCall } });
2775
+ method.postMessage = jest.fn();
2776
+
2777
+ method.init();
2778
+ await method.run();
2779
+
2780
+ expect(typedCall).toHaveBeenCalledWith('FilesystemFileWrite', 'FilesystemFile', {
2781
+ file: {
2782
+ path: 'vol1:test.bin',
2783
+ offset: 1,
2784
+ total_size: 2,
2785
+ data: new Uint8Array([1]),
2786
+ },
2787
+ overwrite: false,
2788
+ append: false,
2789
+ ui_percentage: 99,
2790
+ }, { timeoutMs: undefined });
2791
+ expect(method.postMessage).toHaveBeenCalledWith({
2792
+ event: 'UI_EVENT',
2793
+ type: UI_REQUEST.DEVICE_PROGRESS,
2794
+ payload: expect.objectContaining({
2795
+ progress: 100,
2796
+ transferredBytes: 1,
2797
+ totalBytes: 1,
2798
+ elapsedMs: expect.any(Number),
2799
+ }),
2800
+ });
2801
+ });
2802
+
2803
+ test('splits data larger than the Protocol V2 file payload limit', async () => {
2804
+ const data = new Uint8Array(4097);
2805
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
2806
+ const method = new FileWrite({
2807
+ id: 1,
2808
+ payload: {
2809
+ method: 'fileWrite',
2810
+ path: 'vol1:test.bin',
2811
+ offset: 0,
2812
+ totalSize: 4097,
2813
+ data,
2814
+ },
2815
+ });
2816
+ (method as any).device = stubDevice({ commands: { typedCall } });
2817
+ method.postMessage = jest.fn();
2818
+
2819
+ method.init();
2820
+ const result = await method.run();
2821
+
2822
+ expect(typedCall).toHaveBeenCalledTimes(2);
2823
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemFileWrite', 'FilesystemFile', {
2824
+ file: {
2825
+ path: 'vol1:test.bin',
2826
+ offset: 0,
2827
+ total_size: 4097,
2828
+ data: data.slice(0, 4096),
2829
+ },
2830
+ overwrite: true,
2831
+ append: false,
2832
+ ui_percentage: 99,
2833
+ }, { timeoutMs: undefined });
2834
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileWrite', 'FilesystemFile', {
2835
+ file: {
2836
+ path: 'vol1:test.bin',
2837
+ offset: 4096,
2838
+ total_size: 4097,
2839
+ data: data.slice(4096),
2840
+ },
2841
+ overwrite: false,
2842
+ append: false,
2843
+ ui_percentage: 99,
2844
+ }, { timeoutMs: undefined });
2845
+ expect(result).toMatchObject({
2846
+ path: 'vol1:test.bin',
2847
+ processed_byte: 4097,
2848
+ chunks: 2,
2849
+ });
2850
+ expect(method.postMessage).toHaveBeenNthCalledWith(1, {
2851
+ event: 'UI_EVENT',
2852
+ type: UI_REQUEST.DEVICE_PROGRESS,
2853
+ payload: expect.objectContaining({
2854
+ progress: 99,
2855
+ transferredBytes: 4096,
2856
+ totalBytes: 4097,
2857
+ elapsedMs: expect.any(Number),
2858
+ }),
2859
+ });
2860
+ expect(method.postMessage).toHaveBeenNthCalledWith(2, {
2861
+ event: 'UI_EVENT',
2862
+ type: UI_REQUEST.DEVICE_PROGRESS,
2863
+ payload: expect.objectContaining({
2864
+ progress: 100,
2865
+ transferredBytes: 4097,
2866
+ totalBytes: 4097,
2867
+ elapsedMs: expect.any(Number),
2868
+ }),
2869
+ });
2870
+ });
2871
+
2872
+ test('uses the BLE chunk limit by default in BLE environments', async () => {
2873
+ const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native');
2874
+ const data = new Uint8Array(1801);
2875
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
2876
+ const method = new FileWrite({
2877
+ id: 1,
2878
+ payload: {
2879
+ method: 'fileWrite',
2880
+ path: 'vol1:test.bin',
2881
+ offset: 0,
2882
+ totalSize: 1801,
2883
+ data,
2884
+ },
2885
+ });
2886
+ (method as any).device = stubDevice({ commands: { typedCall } });
2887
+ method.postMessage = jest.fn();
2888
+
2889
+ try {
2890
+ method.init();
2891
+ await method.run();
2892
+ } finally {
2893
+ getSettingsSpy.mockRestore();
2894
+ }
2895
+
2896
+ expect(typedCall).toHaveBeenCalledTimes(2);
2897
+ expect(typedCall.mock.calls[0][2].file.data.byteLength).toBe(1800);
2898
+ expect(typedCall.mock.calls[1][2].file.offset).toBe(1800);
2899
+ expect(typedCall.mock.calls[1][2].file.data.byteLength).toBe(1);
2900
+ });
2901
+ });
2902
+
2903
+ describe('Protocol V2 file read method', () => {
2904
+ test('rejects invalid read and directory parameters before transport call', () => {
2905
+ expect(() => {
2906
+ const method = new FileRead({
2907
+ id: 1,
2908
+ payload: {
2909
+ method: 'fileRead',
2910
+ path: '',
2911
+ offset: 0,
2912
+ },
2913
+ });
2914
+ method.init();
2915
+ }).toThrow(
2916
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
2917
+ );
2918
+
2919
+ expect(() => {
2920
+ const method = new FileRead({
2921
+ id: 1,
2922
+ payload: {
2923
+ method: 'fileRead',
2924
+ path: 'vol1:test.bin',
2925
+ totalSize: -1,
2926
+ },
2927
+ });
2928
+ method.init();
2929
+ }).toThrow(
2930
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
2931
+ );
2932
+
2933
+ expect(() => {
2934
+ const method = new DirList({
2935
+ id: 1,
2936
+ payload: {
2937
+ method: 'dirList',
2938
+ path: 'vol1:',
2939
+ depth: -1,
2940
+ },
2941
+ });
2942
+ method.init();
2943
+ }).toThrow(
2944
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
2945
+ );
2946
+ });
2947
+
2948
+ test('reads full file in chunks when read length is 0', async () => {
2949
+ const firstChunk = new Uint8Array(64).fill(1);
2950
+ const typedCall = jest
2951
+ .fn()
2952
+ .mockResolvedValueOnce({ message: { exist: true, size: 65, directory: false } })
2953
+ .mockResolvedValueOnce({ message: { data: firstChunk } })
2954
+ .mockResolvedValueOnce({ message: { data: new Uint8Array([2]) } });
2955
+ const method = new FileRead({
2956
+ id: 1,
2957
+ payload: {
2958
+ method: 'fileRead',
2959
+ path: 'vol1:test.bin',
2960
+ offset: 0,
2961
+ totalSize: 0,
2962
+ chunkLen: 64,
2963
+ },
2964
+ });
2965
+ (method as any).device = stubDevice({ commands: { typedCall } });
2966
+
2967
+ method.init();
2968
+ const result = await method.run();
2969
+
2970
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemPathInfoQuery', 'FilesystemPathInfo', {
2971
+ path: 'vol1:test.bin',
2972
+ });
2973
+ expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileRead', 'FilesystemFile', {
2974
+ file: {
2975
+ path: 'vol1:test.bin',
2976
+ offset: 0,
2977
+ total_size: 0,
2978
+ },
2979
+ chunk_len: 64,
2980
+ ui_percentage: 99,
2981
+ });
2982
+ expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', {
2983
+ file: {
2984
+ path: 'vol1:test.bin',
2985
+ offset: 64,
2986
+ total_size: 0,
2987
+ },
2988
+ chunk_len: 1,
2989
+ ui_percentage: 99,
2990
+ });
2991
+ expect(result.data.byteLength).toBe(65);
2992
+ expect(result.data[0]).toBe(1);
2993
+ expect(result.data[64]).toBe(2);
2994
+ expect(result).toMatchObject({
2995
+ path: 'vol1:test.bin',
2996
+ offset: 0,
2997
+ total_size: 65,
2998
+ chunks: 2,
2999
+ });
3000
+ });
3001
+
3002
+ test('decodes protobuf bytes hex string returned by transport', async () => {
3003
+ const typedCall = jest.fn().mockResolvedValue({
3004
+ message: {
3005
+ data: '0102ff',
3006
+ },
3007
+ });
3008
+ const method = new FileRead({
3009
+ id: 1,
3010
+ payload: {
3011
+ method: 'fileRead',
3012
+ path: 'vol0:test.bin',
3013
+ offset: 0,
3014
+ totalSize: 3,
3015
+ chunkLen: 512,
3016
+ },
3017
+ });
3018
+ (method as any).device = stubDevice({ commands: { typedCall } });
3019
+
3020
+ method.init();
3021
+ const result = await method.run();
3022
+
3023
+ expect(result.data).toEqual(new Uint8Array([1, 2, 255]));
3024
+ });
3025
+ });