@metamask-previews/perps-controller 0.0.0-preview-ae46ab603 → 0.0.0-preview-e4aa1532

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 (498) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/PerpsController.cjs +2224 -66
  3. package/dist/PerpsController.cjs.map +1 -1
  4. package/dist/PerpsController.d.cts +933 -79
  5. package/dist/PerpsController.d.cts.map +1 -1
  6. package/dist/PerpsController.d.mts +933 -79
  7. package/dist/PerpsController.d.mts.map +1 -1
  8. package/dist/PerpsController.mjs +2221 -64
  9. package/dist/PerpsController.mjs.map +1 -1
  10. package/dist/__mocks__/hyperliquidMock.cjs +68 -0
  11. package/dist/__mocks__/hyperliquidMock.cjs.map +1 -0
  12. package/dist/__mocks__/hyperliquidMock.d.cts +15 -0
  13. package/dist/__mocks__/hyperliquidMock.d.cts.map +1 -0
  14. package/dist/__mocks__/hyperliquidMock.d.mts +15 -0
  15. package/dist/__mocks__/hyperliquidMock.d.mts.map +1 -0
  16. package/dist/__mocks__/hyperliquidMock.mjs +65 -0
  17. package/dist/__mocks__/hyperliquidMock.mjs.map +1 -0
  18. package/dist/__mocks__/providerMocks.cjs +144 -0
  19. package/dist/__mocks__/providerMocks.cjs.map +1 -0
  20. package/dist/__mocks__/providerMocks.d.cts +44 -0
  21. package/dist/__mocks__/providerMocks.d.cts.map +1 -0
  22. package/dist/__mocks__/providerMocks.d.mts +44 -0
  23. package/dist/__mocks__/providerMocks.d.mts.map +1 -0
  24. package/dist/__mocks__/providerMocks.mjs +136 -0
  25. package/dist/__mocks__/providerMocks.mjs.map +1 -0
  26. package/dist/__mocks__/serviceMocks.cjs +187 -0
  27. package/dist/__mocks__/serviceMocks.cjs.map +1 -0
  28. package/dist/__mocks__/serviceMocks.d.cts +58 -0
  29. package/dist/__mocks__/serviceMocks.d.cts.map +1 -0
  30. package/dist/__mocks__/serviceMocks.d.mts +58 -0
  31. package/dist/__mocks__/serviceMocks.d.mts.map +1 -0
  32. package/dist/__mocks__/serviceMocks.mjs +180 -0
  33. package/dist/__mocks__/serviceMocks.mjs.map +1 -0
  34. package/dist/aggregation/SubscriptionMultiplexer.cjs +459 -0
  35. package/dist/aggregation/SubscriptionMultiplexer.cjs.map +1 -0
  36. package/dist/aggregation/SubscriptionMultiplexer.d.cts +222 -0
  37. package/dist/aggregation/SubscriptionMultiplexer.d.cts.map +1 -0
  38. package/dist/aggregation/SubscriptionMultiplexer.d.mts +222 -0
  39. package/dist/aggregation/SubscriptionMultiplexer.d.mts.map +1 -0
  40. package/dist/aggregation/SubscriptionMultiplexer.mjs +455 -0
  41. package/dist/aggregation/SubscriptionMultiplexer.mjs.map +1 -0
  42. package/dist/aggregation/index.cjs +9 -0
  43. package/dist/aggregation/index.cjs.map +1 -0
  44. package/dist/aggregation/index.d.cts +6 -0
  45. package/dist/aggregation/index.d.cts.map +1 -0
  46. package/dist/aggregation/index.d.mts +6 -0
  47. package/dist/aggregation/index.d.mts.map +1 -0
  48. package/dist/aggregation/index.mjs +5 -0
  49. package/dist/aggregation/index.mjs.map +1 -0
  50. package/dist/constants/eventNames.cjs +117 -0
  51. package/dist/constants/eventNames.cjs.map +1 -0
  52. package/dist/constants/eventNames.d.cts +106 -0
  53. package/dist/constants/eventNames.d.cts.map +1 -0
  54. package/dist/constants/eventNames.d.mts +106 -0
  55. package/dist/constants/eventNames.d.mts.map +1 -0
  56. package/dist/constants/eventNames.mjs +114 -0
  57. package/dist/constants/eventNames.mjs.map +1 -0
  58. package/dist/constants/hyperLiquidConfig.cjs +366 -0
  59. package/dist/constants/hyperLiquidConfig.cjs.map +1 -0
  60. package/dist/constants/hyperLiquidConfig.d.cts +276 -0
  61. package/dist/constants/hyperLiquidConfig.d.cts.map +1 -0
  62. package/dist/constants/hyperLiquidConfig.d.mts +276 -0
  63. package/dist/constants/hyperLiquidConfig.d.mts.map +1 -0
  64. package/dist/constants/hyperLiquidConfig.mjs +357 -0
  65. package/dist/constants/hyperLiquidConfig.mjs.map +1 -0
  66. package/dist/constants/index.cjs +23 -0
  67. package/dist/constants/index.cjs.map +1 -0
  68. package/dist/constants/index.d.cts +7 -0
  69. package/dist/constants/index.d.cts.map +1 -0
  70. package/dist/constants/index.d.mts +7 -0
  71. package/dist/constants/index.d.mts.map +1 -0
  72. package/dist/constants/index.mjs +7 -0
  73. package/dist/constants/index.mjs.map +1 -0
  74. package/dist/constants/orderTypes.cjs +31 -0
  75. package/dist/constants/orderTypes.cjs.map +1 -0
  76. package/dist/constants/orderTypes.d.cts +19 -0
  77. package/dist/constants/orderTypes.d.cts.map +1 -0
  78. package/dist/constants/orderTypes.d.mts +19 -0
  79. package/dist/constants/orderTypes.d.mts.map +1 -0
  80. package/dist/constants/orderTypes.mjs +27 -0
  81. package/dist/constants/orderTypes.mjs.map +1 -0
  82. package/dist/constants/performanceMetrics.cjs +61 -0
  83. package/dist/constants/performanceMetrics.cjs.map +1 -0
  84. package/dist/constants/performanceMetrics.d.cts +44 -0
  85. package/dist/constants/performanceMetrics.d.cts.map +1 -0
  86. package/dist/constants/performanceMetrics.d.mts +44 -0
  87. package/dist/constants/performanceMetrics.d.mts.map +1 -0
  88. package/dist/constants/performanceMetrics.mjs +58 -0
  89. package/dist/constants/performanceMetrics.mjs.map +1 -0
  90. package/dist/constants/perpsConfig.cjs +461 -0
  91. package/dist/constants/perpsConfig.cjs.map +1 -0
  92. package/dist/constants/perpsConfig.d.cts +343 -0
  93. package/dist/constants/perpsConfig.d.cts.map +1 -0
  94. package/dist/constants/perpsConfig.d.mts +343 -0
  95. package/dist/constants/perpsConfig.d.mts.map +1 -0
  96. package/dist/constants/perpsConfig.mjs +458 -0
  97. package/dist/constants/perpsConfig.mjs.map +1 -0
  98. package/dist/constants/perpsErrorCodes.cjs +79 -0
  99. package/dist/constants/perpsErrorCodes.cjs.map +1 -0
  100. package/dist/constants/perpsErrorCodes.d.cts +64 -0
  101. package/dist/constants/perpsErrorCodes.d.cts.map +1 -0
  102. package/dist/constants/perpsErrorCodes.d.mts +64 -0
  103. package/dist/constants/perpsErrorCodes.d.mts.map +1 -0
  104. package/dist/constants/perpsErrorCodes.mjs +76 -0
  105. package/dist/constants/perpsErrorCodes.mjs.map +1 -0
  106. package/dist/index.cjs +32 -1
  107. package/dist/index.cjs.map +1 -1
  108. package/dist/index.d.cts +6 -2
  109. package/dist/index.d.cts.map +1 -1
  110. package/dist/index.d.mts +6 -2
  111. package/dist/index.d.mts.map +1 -1
  112. package/dist/index.mjs +9 -1
  113. package/dist/index.mjs.map +1 -1
  114. package/dist/platform-services/HyperLiquidClientService.cjs +857 -0
  115. package/dist/platform-services/HyperLiquidClientService.cjs.map +1 -0
  116. package/dist/platform-services/HyperLiquidClientService.d.cts +263 -0
  117. package/dist/platform-services/HyperLiquidClientService.d.cts.map +1 -0
  118. package/dist/platform-services/HyperLiquidClientService.d.mts +263 -0
  119. package/dist/platform-services/HyperLiquidClientService.d.mts.map +1 -0
  120. package/dist/platform-services/HyperLiquidClientService.mjs +853 -0
  121. package/dist/platform-services/HyperLiquidClientService.mjs.map +1 -0
  122. package/dist/platform-services/HyperLiquidSubscriptionService.cjs +2170 -0
  123. package/dist/platform-services/HyperLiquidSubscriptionService.cjs.map +1 -0
  124. package/dist/platform-services/HyperLiquidSubscriptionService.d.cts +424 -0
  125. package/dist/platform-services/HyperLiquidSubscriptionService.d.cts.map +1 -0
  126. package/dist/platform-services/HyperLiquidSubscriptionService.d.mts +424 -0
  127. package/dist/platform-services/HyperLiquidSubscriptionService.d.mts.map +1 -0
  128. package/dist/platform-services/HyperLiquidSubscriptionService.mjs +2166 -0
  129. package/dist/platform-services/HyperLiquidSubscriptionService.mjs.map +1 -0
  130. package/dist/platform-services/HyperLiquidWalletService.cjs +140 -0
  131. package/dist/platform-services/HyperLiquidWalletService.cjs.map +1 -0
  132. package/dist/platform-services/HyperLiquidWalletService.d.cts +78 -0
  133. package/dist/platform-services/HyperLiquidWalletService.d.cts.map +1 -0
  134. package/dist/platform-services/HyperLiquidWalletService.d.mts +78 -0
  135. package/dist/platform-services/HyperLiquidWalletService.d.mts.map +1 -0
  136. package/dist/platform-services/HyperLiquidWalletService.mjs +136 -0
  137. package/dist/platform-services/HyperLiquidWalletService.mjs.map +1 -0
  138. package/dist/providers/AggregatedPerpsProvider.cjs +777 -0
  139. package/dist/providers/AggregatedPerpsProvider.cjs.map +1 -0
  140. package/dist/providers/AggregatedPerpsProvider.d.cts +466 -0
  141. package/dist/providers/AggregatedPerpsProvider.d.cts.map +1 -0
  142. package/dist/providers/AggregatedPerpsProvider.d.mts +466 -0
  143. package/dist/providers/AggregatedPerpsProvider.d.mts.map +1 -0
  144. package/dist/providers/AggregatedPerpsProvider.mjs +773 -0
  145. package/dist/providers/AggregatedPerpsProvider.mjs.map +1 -0
  146. package/dist/providers/HyperLiquidProvider.cjs +5172 -0
  147. package/dist/providers/HyperLiquidProvider.cjs.map +1 -0
  148. package/dist/providers/HyperLiquidProvider.d.cts +881 -0
  149. package/dist/providers/HyperLiquidProvider.d.cts.map +1 -0
  150. package/dist/providers/HyperLiquidProvider.d.mts +881 -0
  151. package/dist/providers/HyperLiquidProvider.d.mts.map +1 -0
  152. package/dist/providers/HyperLiquidProvider.mjs +5168 -0
  153. package/dist/providers/HyperLiquidProvider.mjs.map +1 -0
  154. package/dist/providers/index.cjs +11 -0
  155. package/dist/providers/index.cjs.map +1 -0
  156. package/dist/providers/index.d.cts +6 -0
  157. package/dist/providers/index.d.cts.map +1 -0
  158. package/dist/providers/index.d.mts +6 -0
  159. package/dist/providers/index.d.mts.map +1 -0
  160. package/dist/providers/index.mjs +6 -0
  161. package/dist/providers/index.mjs.map +1 -0
  162. package/dist/routing/ProviderRouter.cjs +148 -0
  163. package/dist/routing/ProviderRouter.cjs.map +1 -0
  164. package/dist/routing/ProviderRouter.d.cts +124 -0
  165. package/dist/routing/ProviderRouter.d.cts.map +1 -0
  166. package/dist/routing/ProviderRouter.d.mts +124 -0
  167. package/dist/routing/ProviderRouter.d.mts.map +1 -0
  168. package/dist/routing/ProviderRouter.mjs +144 -0
  169. package/dist/routing/ProviderRouter.mjs.map +1 -0
  170. package/dist/routing/index.cjs +9 -0
  171. package/dist/routing/index.cjs.map +1 -0
  172. package/dist/routing/index.d.cts +6 -0
  173. package/dist/routing/index.d.cts.map +1 -0
  174. package/dist/routing/index.d.mts +6 -0
  175. package/dist/routing/index.d.mts.map +1 -0
  176. package/dist/routing/index.mjs +5 -0
  177. package/dist/routing/index.mjs.map +1 -0
  178. package/dist/selectors.cjs +161 -0
  179. package/dist/selectors.cjs.map +1 -0
  180. package/dist/selectors.d.cts +345 -0
  181. package/dist/selectors.d.cts.map +1 -0
  182. package/dist/selectors.d.mts +345 -0
  183. package/dist/selectors.d.mts.map +1 -0
  184. package/dist/selectors.mjs +153 -0
  185. package/dist/selectors.mjs.map +1 -0
  186. package/dist/services/AccountService.cjs +313 -0
  187. package/dist/services/AccountService.cjs.map +1 -0
  188. package/dist/services/AccountService.d.cts +53 -0
  189. package/dist/services/AccountService.d.cts.map +1 -0
  190. package/dist/services/AccountService.d.mts +53 -0
  191. package/dist/services/AccountService.d.mts.map +1 -0
  192. package/dist/services/AccountService.mjs +309 -0
  193. package/dist/services/AccountService.mjs.map +1 -0
  194. package/dist/services/DataLakeService.cjs +219 -0
  195. package/dist/services/DataLakeService.cjs.map +1 -0
  196. package/dist/services/DataLakeService.d.cts +49 -0
  197. package/dist/services/DataLakeService.d.cts.map +1 -0
  198. package/dist/services/DataLakeService.d.mts +49 -0
  199. package/dist/services/DataLakeService.d.mts.map +1 -0
  200. package/dist/services/DataLakeService.mjs +215 -0
  201. package/dist/services/DataLakeService.mjs.map +1 -0
  202. package/dist/services/DepositService.cjs +96 -0
  203. package/dist/services/DepositService.cjs.map +1 -0
  204. package/dist/services/DepositService.d.cts +37 -0
  205. package/dist/services/DepositService.d.cts.map +1 -0
  206. package/dist/services/DepositService.d.mts +37 -0
  207. package/dist/services/DepositService.d.mts.map +1 -0
  208. package/dist/services/DepositService.mjs +92 -0
  209. package/dist/services/DepositService.mjs.map +1 -0
  210. package/dist/services/EligibilityService.cjs +156 -0
  211. package/dist/services/EligibilityService.cjs.map +1 -0
  212. package/dist/services/EligibilityService.d.cts +48 -0
  213. package/dist/services/EligibilityService.d.cts.map +1 -0
  214. package/dist/services/EligibilityService.d.mts +48 -0
  215. package/dist/services/EligibilityService.d.mts.map +1 -0
  216. package/dist/services/EligibilityService.mjs +152 -0
  217. package/dist/services/EligibilityService.mjs.map +1 -0
  218. package/dist/services/FeatureFlagConfigurationService.cjs +258 -0
  219. package/dist/services/FeatureFlagConfigurationService.cjs.map +1 -0
  220. package/dist/services/FeatureFlagConfigurationService.d.cts +100 -0
  221. package/dist/services/FeatureFlagConfigurationService.d.cts.map +1 -0
  222. package/dist/services/FeatureFlagConfigurationService.d.mts +100 -0
  223. package/dist/services/FeatureFlagConfigurationService.d.mts.map +1 -0
  224. package/dist/services/FeatureFlagConfigurationService.mjs +254 -0
  225. package/dist/services/FeatureFlagConfigurationService.mjs.map +1 -0
  226. package/dist/services/MarketDataService.cjs +875 -0
  227. package/dist/services/MarketDataService.cjs.map +1 -0
  228. package/dist/services/MarketDataService.d.cts +289 -0
  229. package/dist/services/MarketDataService.d.cts.map +1 -0
  230. package/dist/services/MarketDataService.d.mts +289 -0
  231. package/dist/services/MarketDataService.d.mts.map +1 -0
  232. package/dist/services/MarketDataService.mjs +871 -0
  233. package/dist/services/MarketDataService.mjs.map +1 -0
  234. package/dist/services/RewardsIntegrationService.cjs +94 -0
  235. package/dist/services/RewardsIntegrationService.cjs.map +1 -0
  236. package/dist/services/RewardsIntegrationService.d.cts +30 -0
  237. package/dist/services/RewardsIntegrationService.d.cts.map +1 -0
  238. package/dist/services/RewardsIntegrationService.d.mts +30 -0
  239. package/dist/services/RewardsIntegrationService.d.mts.map +1 -0
  240. package/dist/services/RewardsIntegrationService.mjs +90 -0
  241. package/dist/services/RewardsIntegrationService.mjs.map +1 -0
  242. package/dist/services/ServiceContext.cjs +3 -0
  243. package/dist/services/ServiceContext.cjs.map +1 -0
  244. package/dist/services/ServiceContext.d.cts +110 -0
  245. package/dist/services/ServiceContext.d.cts.map +1 -0
  246. package/dist/services/ServiceContext.d.mts +110 -0
  247. package/dist/services/ServiceContext.d.mts.map +1 -0
  248. package/dist/services/ServiceContext.mjs +2 -0
  249. package/dist/services/ServiceContext.mjs.map +1 -0
  250. package/dist/services/TradingService.cjs +1481 -0
  251. package/dist/services/TradingService.cjs.map +1 -0
  252. package/dist/services/TradingService.d.cts +308 -0
  253. package/dist/services/TradingService.d.cts.map +1 -0
  254. package/dist/services/TradingService.d.mts +308 -0
  255. package/dist/services/TradingService.d.mts.map +1 -0
  256. package/dist/services/TradingService.mjs +1477 -0
  257. package/dist/services/TradingService.mjs.map +1 -0
  258. package/dist/services/index.cjs +26 -0
  259. package/dist/services/index.cjs.map +1 -0
  260. package/dist/services/index.d.cts +11 -0
  261. package/dist/services/index.d.cts.map +1 -0
  262. package/dist/services/index.d.mts +11 -0
  263. package/dist/services/index.d.mts.map +1 -0
  264. package/dist/services/index.mjs +10 -0
  265. package/dist/services/index.mjs.map +1 -0
  266. package/dist/services/utils.cjs +16 -0
  267. package/dist/services/utils.cjs.map +1 -0
  268. package/dist/services/utils.d.cts +8 -0
  269. package/dist/services/utils.d.cts.map +1 -0
  270. package/dist/services/utils.d.mts +8 -0
  271. package/dist/services/utils.d.mts.map +1 -0
  272. package/dist/services/utils.mjs +12 -0
  273. package/dist/services/utils.mjs.map +1 -0
  274. package/dist/types/chart.cjs +42 -0
  275. package/dist/types/chart.cjs.map +1 -0
  276. package/dist/types/chart.d.cts +62 -0
  277. package/dist/types/chart.d.cts.map +1 -0
  278. package/dist/types/chart.d.mts +62 -0
  279. package/dist/types/chart.d.mts.map +1 -0
  280. package/dist/types/chart.mjs +39 -0
  281. package/dist/types/chart.mjs.map +1 -0
  282. package/dist/types/hyperliquid-types.cjs +3 -0
  283. package/dist/types/hyperliquid-types.cjs.map +1 -0
  284. package/dist/types/hyperliquid-types.d.cts +52 -0
  285. package/dist/types/hyperliquid-types.d.cts.map +1 -0
  286. package/dist/types/hyperliquid-types.d.mts +52 -0
  287. package/dist/types/hyperliquid-types.d.mts.map +1 -0
  288. package/dist/types/hyperliquid-types.mjs +2 -0
  289. package/dist/types/hyperliquid-types.mjs.map +1 -0
  290. package/dist/types/index.cjs +96 -0
  291. package/dist/types/index.cjs.map +1 -0
  292. package/dist/types/index.d.cts +1153 -0
  293. package/dist/types/index.d.cts.map +1 -0
  294. package/dist/types/index.d.mts +1153 -0
  295. package/dist/types/index.d.mts.map +1 -0
  296. package/dist/types/index.mjs +79 -0
  297. package/dist/types/index.mjs.map +1 -0
  298. package/dist/types/transactionTypes.cjs +28 -0
  299. package/dist/types/transactionTypes.cjs.map +1 -0
  300. package/dist/types/transactionTypes.d.cts +64 -0
  301. package/dist/types/transactionTypes.d.cts.map +1 -0
  302. package/dist/types/transactionTypes.d.mts +64 -0
  303. package/dist/types/transactionTypes.d.mts.map +1 -0
  304. package/dist/types/transactionTypes.mjs +23 -0
  305. package/dist/types/transactionTypes.mjs.map +1 -0
  306. package/dist/utils/accountUtils.cjs +65 -0
  307. package/dist/utils/accountUtils.cjs.map +1 -0
  308. package/dist/utils/accountUtils.d.cts +26 -0
  309. package/dist/utils/accountUtils.d.cts.map +1 -0
  310. package/dist/utils/accountUtils.d.mts +26 -0
  311. package/dist/utils/accountUtils.d.mts.map +1 -0
  312. package/dist/utils/accountUtils.mjs +61 -0
  313. package/dist/utils/accountUtils.mjs.map +1 -0
  314. package/dist/utils/amountConversion.cjs +61 -0
  315. package/dist/utils/amountConversion.cjs.map +1 -0
  316. package/dist/utils/amountConversion.d.cts +16 -0
  317. package/dist/utils/amountConversion.d.cts.map +1 -0
  318. package/dist/utils/amountConversion.d.mts +16 -0
  319. package/dist/utils/amountConversion.d.mts.map +1 -0
  320. package/dist/utils/amountConversion.mjs +54 -0
  321. package/dist/utils/amountConversion.mjs.map +1 -0
  322. package/dist/utils/chartUtils.cjs +98 -0
  323. package/dist/utils/chartUtils.cjs.map +1 -0
  324. package/dist/utils/chartUtils.d.cts +14 -0
  325. package/dist/utils/chartUtils.d.cts.map +1 -0
  326. package/dist/utils/chartUtils.d.mts +14 -0
  327. package/dist/utils/chartUtils.d.mts.map +1 -0
  328. package/dist/utils/chartUtils.mjs +94 -0
  329. package/dist/utils/chartUtils.mjs.map +1 -0
  330. package/dist/utils/errorUtils.cjs +22 -0
  331. package/dist/utils/errorUtils.cjs.map +1 -0
  332. package/dist/utils/errorUtils.d.cts +13 -0
  333. package/dist/utils/errorUtils.d.cts.map +1 -0
  334. package/dist/utils/errorUtils.d.mts +13 -0
  335. package/dist/utils/errorUtils.d.mts.map +1 -0
  336. package/dist/utils/errorUtils.mjs +18 -0
  337. package/dist/utils/errorUtils.mjs.map +1 -0
  338. package/dist/utils/formatUtils.cjs +987 -0
  339. package/dist/utils/formatUtils.cjs.map +1 -0
  340. package/dist/utils/formatUtils.d.cts +387 -0
  341. package/dist/utils/formatUtils.d.cts.map +1 -0
  342. package/dist/utils/formatUtils.d.mts +387 -0
  343. package/dist/utils/formatUtils.d.mts.map +1 -0
  344. package/dist/utils/formatUtils.mjs +966 -0
  345. package/dist/utils/formatUtils.mjs.map +1 -0
  346. package/dist/utils/hyperLiquidAdapter.cjs +454 -0
  347. package/dist/utils/hyperLiquidAdapter.cjs.map +1 -0
  348. package/dist/utils/hyperLiquidAdapter.d.cts +158 -0
  349. package/dist/utils/hyperLiquidAdapter.d.cts.map +1 -0
  350. package/dist/utils/hyperLiquidAdapter.d.mts +158 -0
  351. package/dist/utils/hyperLiquidAdapter.d.mts.map +1 -0
  352. package/dist/utils/hyperLiquidAdapter.mjs +439 -0
  353. package/dist/utils/hyperLiquidAdapter.mjs.map +1 -0
  354. package/dist/utils/hyperLiquidOrderBookProcessor.cjs +89 -0
  355. package/dist/utils/hyperLiquidOrderBookProcessor.cjs.map +1 -0
  356. package/dist/utils/hyperLiquidOrderBookProcessor.d.cts +58 -0
  357. package/dist/utils/hyperLiquidOrderBookProcessor.d.cts.map +1 -0
  358. package/dist/utils/hyperLiquidOrderBookProcessor.d.mts +58 -0
  359. package/dist/utils/hyperLiquidOrderBookProcessor.d.mts.map +1 -0
  360. package/dist/utils/hyperLiquidOrderBookProcessor.mjs +84 -0
  361. package/dist/utils/hyperLiquidOrderBookProcessor.mjs.map +1 -0
  362. package/dist/utils/hyperLiquidValidation.cjs +426 -0
  363. package/dist/utils/hyperLiquidValidation.cjs.map +1 -0
  364. package/dist/utils/hyperLiquidValidation.d.cts +143 -0
  365. package/dist/utils/hyperLiquidValidation.d.cts.map +1 -0
  366. package/dist/utils/hyperLiquidValidation.d.mts +143 -0
  367. package/dist/utils/hyperLiquidValidation.d.mts.map +1 -0
  368. package/dist/utils/hyperLiquidValidation.mjs +413 -0
  369. package/dist/utils/hyperLiquidValidation.mjs.map +1 -0
  370. package/dist/utils/idUtils.cjs +50 -0
  371. package/dist/utils/idUtils.cjs.map +1 -0
  372. package/dist/utils/idUtils.d.cts +38 -0
  373. package/dist/utils/idUtils.d.cts.map +1 -0
  374. package/dist/utils/idUtils.d.mts +38 -0
  375. package/dist/utils/idUtils.d.mts.map +1 -0
  376. package/dist/utils/idUtils.mjs +42 -0
  377. package/dist/utils/idUtils.mjs.map +1 -0
  378. package/dist/utils/marginUtils.cjs +137 -0
  379. package/dist/utils/marginUtils.cjs.map +1 -0
  380. package/dist/utils/marginUtils.d.cts +64 -0
  381. package/dist/utils/marginUtils.d.cts.map +1 -0
  382. package/dist/utils/marginUtils.d.mts +64 -0
  383. package/dist/utils/marginUtils.d.mts.map +1 -0
  384. package/dist/utils/marginUtils.mjs +131 -0
  385. package/dist/utils/marginUtils.mjs.map +1 -0
  386. package/dist/utils/marketDataTransform.cjs +227 -0
  387. package/dist/utils/marketDataTransform.cjs.map +1 -0
  388. package/dist/utils/marketDataTransform.d.cts +45 -0
  389. package/dist/utils/marketDataTransform.d.cts.map +1 -0
  390. package/dist/utils/marketDataTransform.d.mts +45 -0
  391. package/dist/utils/marketDataTransform.d.mts.map +1 -0
  392. package/dist/utils/marketDataTransform.mjs +220 -0
  393. package/dist/utils/marketDataTransform.mjs.map +1 -0
  394. package/dist/utils/marketUtils.cjs +420 -0
  395. package/dist/utils/marketUtils.cjs.map +1 -0
  396. package/dist/utils/marketUtils.d.cts +227 -0
  397. package/dist/utils/marketUtils.d.cts.map +1 -0
  398. package/dist/utils/marketUtils.d.mts +227 -0
  399. package/dist/utils/marketUtils.d.mts.map +1 -0
  400. package/dist/utils/marketUtils.mjs +404 -0
  401. package/dist/utils/marketUtils.mjs.map +1 -0
  402. package/dist/utils/orderBookGrouping.cjs +151 -0
  403. package/dist/utils/orderBookGrouping.cjs.map +1 -0
  404. package/dist/utils/orderBookGrouping.d.cts +76 -0
  405. package/dist/utils/orderBookGrouping.d.cts.map +1 -0
  406. package/dist/utils/orderBookGrouping.d.mts +76 -0
  407. package/dist/utils/orderBookGrouping.d.mts.map +1 -0
  408. package/dist/utils/orderBookGrouping.mjs +143 -0
  409. package/dist/utils/orderBookGrouping.mjs.map +1 -0
  410. package/dist/utils/orderCalculations.cjs +283 -0
  411. package/dist/utils/orderCalculations.cjs.map +1 -0
  412. package/dist/utils/orderCalculations.d.cts +107 -0
  413. package/dist/utils/orderCalculations.d.cts.map +1 -0
  414. package/dist/utils/orderCalculations.d.mts +107 -0
  415. package/dist/utils/orderCalculations.d.mts.map +1 -0
  416. package/dist/utils/orderCalculations.mjs +274 -0
  417. package/dist/utils/orderCalculations.mjs.map +1 -0
  418. package/dist/utils/orderUtils.cjs +159 -0
  419. package/dist/utils/orderUtils.cjs.map +1 -0
  420. package/dist/utils/orderUtils.d.cts +66 -0
  421. package/dist/utils/orderUtils.d.cts.map +1 -0
  422. package/dist/utils/orderUtils.d.mts +66 -0
  423. package/dist/utils/orderUtils.d.mts.map +1 -0
  424. package/dist/utils/orderUtils.mjs +151 -0
  425. package/dist/utils/orderUtils.mjs.map +1 -0
  426. package/dist/utils/pnlCalculations.cjs +107 -0
  427. package/dist/utils/pnlCalculations.cjs.map +1 -0
  428. package/dist/utils/pnlCalculations.d.cts +79 -0
  429. package/dist/utils/pnlCalculations.d.cts.map +1 -0
  430. package/dist/utils/pnlCalculations.d.mts +79 -0
  431. package/dist/utils/pnlCalculations.d.mts.map +1 -0
  432. package/dist/utils/pnlCalculations.mjs +97 -0
  433. package/dist/utils/pnlCalculations.mjs.map +1 -0
  434. package/dist/utils/positionCalculations.cjs +176 -0
  435. package/dist/utils/positionCalculations.cjs.map +1 -0
  436. package/dist/utils/positionCalculations.d.cts +85 -0
  437. package/dist/utils/positionCalculations.d.cts.map +1 -0
  438. package/dist/utils/positionCalculations.d.mts +85 -0
  439. package/dist/utils/positionCalculations.d.mts.map +1 -0
  440. package/dist/utils/positionCalculations.mjs +165 -0
  441. package/dist/utils/positionCalculations.mjs.map +1 -0
  442. package/dist/utils/sortMarkets.cjs +88 -0
  443. package/dist/utils/sortMarkets.cjs.map +1 -0
  444. package/dist/utils/sortMarkets.d.cts +21 -0
  445. package/dist/utils/sortMarkets.d.cts.map +1 -0
  446. package/dist/utils/sortMarkets.d.mts +21 -0
  447. package/dist/utils/sortMarkets.d.mts.map +1 -0
  448. package/dist/utils/sortMarkets.mjs +84 -0
  449. package/dist/utils/sortMarkets.mjs.map +1 -0
  450. package/dist/utils/standaloneInfoClient.cjs +34 -0
  451. package/dist/utils/standaloneInfoClient.cjs.map +1 -0
  452. package/dist/utils/standaloneInfoClient.d.cts +31 -0
  453. package/dist/utils/standaloneInfoClient.d.cts.map +1 -0
  454. package/dist/utils/standaloneInfoClient.d.mts +31 -0
  455. package/dist/utils/standaloneInfoClient.d.mts.map +1 -0
  456. package/dist/utils/standaloneInfoClient.mjs +30 -0
  457. package/dist/utils/standaloneInfoClient.mjs.map +1 -0
  458. package/dist/utils/stringParseUtils.cjs +49 -0
  459. package/dist/utils/stringParseUtils.cjs.map +1 -0
  460. package/dist/utils/stringParseUtils.d.cts +35 -0
  461. package/dist/utils/stringParseUtils.d.cts.map +1 -0
  462. package/dist/utils/stringParseUtils.d.mts +35 -0
  463. package/dist/utils/stringParseUtils.d.mts.map +1 -0
  464. package/dist/utils/stringParseUtils.mjs +44 -0
  465. package/dist/utils/stringParseUtils.mjs.map +1 -0
  466. package/dist/utils/time.cjs +37 -0
  467. package/dist/utils/time.cjs.map +1 -0
  468. package/dist/utils/time.d.cts +17 -0
  469. package/dist/utils/time.d.cts.map +1 -0
  470. package/dist/utils/time.d.mts +17 -0
  471. package/dist/utils/time.d.mts.map +1 -0
  472. package/dist/utils/time.mjs +33 -0
  473. package/dist/utils/time.mjs.map +1 -0
  474. package/dist/utils/tpslValidation.cjs +505 -0
  475. package/dist/utils/tpslValidation.cjs.map +1 -0
  476. package/dist/utils/tpslValidation.d.cts +182 -0
  477. package/dist/utils/tpslValidation.d.cts.map +1 -0
  478. package/dist/utils/tpslValidation.d.mts +182 -0
  479. package/dist/utils/tpslValidation.d.mts.map +1 -0
  480. package/dist/utils/tpslValidation.mjs +482 -0
  481. package/dist/utils/tpslValidation.mjs.map +1 -0
  482. package/dist/utils/transactionUtils.cjs +36 -0
  483. package/dist/utils/transactionUtils.cjs.map +1 -0
  484. package/dist/utils/transactionUtils.d.cts +17 -0
  485. package/dist/utils/transactionUtils.d.cts.map +1 -0
  486. package/dist/utils/transactionUtils.d.mts +17 -0
  487. package/dist/utils/transactionUtils.d.mts.map +1 -0
  488. package/dist/utils/transactionUtils.mjs +32 -0
  489. package/dist/utils/transactionUtils.mjs.map +1 -0
  490. package/dist/utils/wait.cjs +12 -0
  491. package/dist/utils/wait.cjs.map +1 -0
  492. package/dist/utils/wait.d.cts +8 -0
  493. package/dist/utils/wait.d.cts.map +1 -0
  494. package/dist/utils/wait.d.mts +8 -0
  495. package/dist/utils/wait.d.mts.map +1 -0
  496. package/dist/utils/wait.mjs +8 -0
  497. package/dist/utils/wait.mjs.map +1 -0
  498. package/package.json +19 -4
@@ -0,0 +1,2170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HyperLiquidSubscriptionService = void 0;
4
+ const perpsConfig_1 = require("../constants/perpsConfig.cjs");
5
+ const accountUtils_1 = require("../utils/accountUtils.cjs");
6
+ const hyperLiquidAdapter_1 = require("../utils/hyperLiquidAdapter.cjs");
7
+ const hyperLiquidOrderBookProcessor_1 = require("../utils/hyperLiquidOrderBookProcessor.cjs");
8
+ const marketDataTransform_1 = require("../utils/marketDataTransform.cjs");
9
+ /**
10
+ * Ensures we have a proper Error object for logging.
11
+ * Converts unknown/string errors to proper Error instances.
12
+ *
13
+ * @param error - The caught error (could be Error, string, or unknown)
14
+ * @returns A proper Error instance
15
+ */
16
+ function ensureError(error) {
17
+ if (error instanceof Error) {
18
+ return error;
19
+ }
20
+ return new Error(String(error));
21
+ }
22
+ /**
23
+ * Service for managing HyperLiquid WebSocket subscriptions
24
+ * Implements singleton subscription architecture with reference counting
25
+ */
26
+ class HyperLiquidSubscriptionService {
27
+ constructor(clientService, walletService, platformDependencies, hip3Enabled, enabledDexs, allowlistMarkets, blocklistMarkets) {
28
+ this.discoveredDexNames = []; // DEX order for mapping webData3 perpDexStates indices
29
+ // DEX discovery synchronization - allows subscriptions to wait for HIP-3 DEX discovery
30
+ this.dexDiscoveryPromise = null;
31
+ this.dexDiscoveryResolver = null;
32
+ // Track DEXs for synchronized position notifications
33
+ // Ensures all DEXs send initial data before notifying subscribers
34
+ this.expectedDexs = new Set();
35
+ this.initializedDexs = new Set();
36
+ // Subscriber collections
37
+ this.priceSubscribers = new Map();
38
+ this.positionSubscribers = new Set();
39
+ // Order fill subscribers keyed by accountId (normalized: undefined -> 'default')
40
+ this.orderFillSubscribers = new Map();
41
+ this.orderSubscribers = new Set();
42
+ this.accountSubscribers = new Set();
43
+ // Track which subscribers want market data
44
+ this.marketDataSubscribers = new Map();
45
+ // Track which subscribers want top-of-book (best bid/ask) data
46
+ this.orderBookSubscribers = new Map();
47
+ this.globalActiveAssetSubscriptions = new Map();
48
+ this.globalBboSubscriptions = new Map();
49
+ // Order fill subscriptions keyed by accountId (normalized: undefined -> 'default')
50
+ this.orderFillSubscriptions = new Map();
51
+ this.symbolSubscriberCounts = new Map();
52
+ this.dexSubscriberCounts = new Map(); // Track subscribers per DEX for assetCtxs
53
+ // Multi-DEX webData3 subscription for all user data (positions, orders, account, OI caps)
54
+ this.webData3Subscriptions = new Map(); // Key: dex name ('' for main)
55
+ this.positionSubscriberCount = 0;
56
+ this.orderSubscriberCount = 0;
57
+ this.accountSubscriberCount = 0;
58
+ this.oiCapSubscriberCount = 0;
59
+ // Multi-DEX data caches
60
+ this.dexPositionsCache = new Map(); // Per-DEX positions
61
+ this.dexOrdersCache = new Map(); // Per-DEX orders
62
+ this.dexAccountCache = new Map(); // Per-DEX account state
63
+ this.cachedPositions = null; // Aggregated positions
64
+ this.cachedOrders = null; // Aggregated orders
65
+ this.cachedAccount = null; // Aggregated account
66
+ this.ordersCacheInitialized = false; // Track if orders cache has received WebSocket data
67
+ this.positionsCacheInitialized = false; // Track if positions cache has received WebSocket data
68
+ // OI Cap tracking (from webData3.perpDexStates[].perpsAtOpenInterestCap)
69
+ this.oiCapSubscribers = new Set();
70
+ this.cachedOICaps = [];
71
+ this.cachedOICapsHash = '';
72
+ this.oiCapsCacheInitialized = false;
73
+ // Global price data cache
74
+ this.cachedPriceData = null;
75
+ // HIP-3: assetCtxs subscriptions for multi-DEX market data
76
+ this.assetCtxsSubscriptions = new Map(); // Key: dex name ('' for main)
77
+ this.dexAssetCtxsCache = new Map(); // Per-DEX asset contexts
78
+ this.assetCtxsSubscriptionPromises = new Map(); // Track in-progress subscriptions
79
+ this.clearinghouseStateSubscriptions = new Map(); // Key: dex name ('' for main)
80
+ this.openOrdersSubscriptions = new Map(); // Key: dex name ('' for main)
81
+ // Meta cache per DEX - populated by metaAndAssetCtxs, used by createAssetCtxsSubscription
82
+ // This avoids redundant meta() API calls since metaAndAssetCtxs already returns meta data
83
+ this.dexMetaCache = new Map();
84
+ // Order book data cache
85
+ this.orderBookCache = new Map();
86
+ // Market data caching for multi-channel consolidation
87
+ this.marketDataCache = new Map();
88
+ // Cache hashes to avoid recomputation
89
+ this.cachedPositionsHash = '';
90
+ this.cachedOrdersHash = '';
91
+ this.cachedAccountHash = '';
92
+ this.clientService = clientService;
93
+ this.walletService = walletService;
94
+ this.deps = platformDependencies;
95
+ this.hip3Enabled = hip3Enabled ?? false;
96
+ this.enabledDexs = enabledDexs ?? [];
97
+ this.discoveredDexNames = enabledDexs ?? [];
98
+ this.allowlistMarkets = allowlistMarkets ?? [];
99
+ this.blocklistMarkets = blocklistMarkets ?? [];
100
+ }
101
+ /**
102
+ * Get error context for logging with searchable tags and context.
103
+ * Enables Sentry dashboard filtering by feature, provider, and network.
104
+ *
105
+ * @param method - The method name where the error occurred
106
+ * @param extra - Optional additional context fields (merged into searchable context.data)
107
+ * @returns Error options with tags (searchable) and context (searchable)
108
+ */
109
+ getErrorContext(method, extra) {
110
+ return {
111
+ tags: {
112
+ feature: perpsConfig_1.PERPS_CONSTANTS.FeatureName,
113
+ provider: 'hyperliquid',
114
+ network: this.clientService.isTestnetMode() ? 'testnet' : 'mainnet',
115
+ },
116
+ context: {
117
+ name: 'HyperLiquidSubscriptionService',
118
+ data: {
119
+ method,
120
+ ...extra,
121
+ },
122
+ },
123
+ };
124
+ }
125
+ /**
126
+ * Check if a DEX is enabled in our configuration
127
+ * Used to filter webData3 callback data to only process DEXs we care about
128
+ *
129
+ * @param dex - DEX name (null for main DEX, string for HIP-3)
130
+ * @returns true if this DEX should be processed
131
+ */
132
+ isDexEnabled(dex) {
133
+ if (dex === null) {
134
+ return true; // Main DEX always enabled
135
+ }
136
+ if (!this.hip3Enabled) {
137
+ return false; // HIP-3 disabled entirely
138
+ }
139
+ return this.enabledDexs.includes(dex);
140
+ }
141
+ /**
142
+ * Populate DEX meta cache with pre-fetched meta data
143
+ * Called by Provider after buildAssetMapping to share cached meta,
144
+ * avoiding redundant metaAndAssetCtxs/meta API calls during subscription setup
145
+ *
146
+ * @param dex - DEX key ('' for main DEX, 'xyz'/'flx'/etc for HIP-3)
147
+ * @param meta - Meta response containing universe data
148
+ * @param meta.universe
149
+ */
150
+ setDexMetaCache(dex, meta) {
151
+ this.dexMetaCache.set(dex, meta);
152
+ this.deps.debugLogger.log('[SubscriptionService] DEX meta cache populated', {
153
+ dex: dex || 'main',
154
+ universeSize: meta.universe.length,
155
+ });
156
+ }
157
+ /**
158
+ * Cache asset contexts for a specific DEX from API response
159
+ * This allows buildAssetMapping() to populate cache for getMarketDataWithPrices() to use
160
+ *
161
+ * @param dex - DEX name ('' for main perps)
162
+ * @param assetCtxs - Asset contexts from metaAndAssetCtxs response
163
+ */
164
+ setDexAssetCtxsCache(dex, assetCtxs) {
165
+ this.dexAssetCtxsCache.set(dex, assetCtxs);
166
+ this.deps.debugLogger.log('[SubscriptionService] DEX assetCtxs cache populated', {
167
+ dex: dex || 'main',
168
+ ctxsCount: assetCtxs.length,
169
+ });
170
+ }
171
+ /**
172
+ * Get cached assetCtxs for a DEX
173
+ * Returns the cached asset contexts from WebSocket subscription if available
174
+ *
175
+ * @param dex - DEX key ('' for main DEX, 'xyz'/'flx'/etc for HIP-3)
176
+ * @returns Array of asset contexts or undefined if not cached
177
+ */
178
+ getDexAssetCtxsCache(dex) {
179
+ return this.dexAssetCtxsCache.get(dex);
180
+ }
181
+ /**
182
+ * Wait for DEX discovery to complete (with timeout)
183
+ * Used when HIP-3 is enabled but enabledDexs hasn't been populated yet.
184
+ * This allows subscriptions to wait for DEX discovery before creating per-DEX subscriptions.
185
+ *
186
+ * @param timeoutMs - Timeout in milliseconds (default: 5000)
187
+ */
188
+ async waitForDexDiscovery(timeoutMs = 5000) {
189
+ // Already have DEXs, no need to wait
190
+ if (this.enabledDexs.length > 0) {
191
+ return;
192
+ }
193
+ // Create promise if not exists
194
+ if (!this.dexDiscoveryPromise) {
195
+ this.dexDiscoveryPromise = new Promise((resolve) => {
196
+ this.dexDiscoveryResolver = resolve;
197
+ });
198
+ }
199
+ // Wait with timeout
200
+ let timeoutId;
201
+ const timeoutPromise = new Promise((_, reject) => {
202
+ timeoutId = setTimeout(() => reject(new Error('DEX discovery timeout')), timeoutMs);
203
+ });
204
+ try {
205
+ await Promise.race([this.dexDiscoveryPromise, timeoutPromise]);
206
+ }
207
+ catch {
208
+ this.deps.debugLogger.log('DEX discovery wait timed out, proceeding with main DEX only');
209
+ }
210
+ finally {
211
+ if (timeoutId) {
212
+ clearTimeout(timeoutId);
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Update feature flags for HIP-3 support
218
+ * Called when provider configuration changes at runtime
219
+ * Note: Market filtering is NOT applied in subscription service - only in Provider
220
+ *
221
+ * @param hip3Enabled - Whether HIP-3 is enabled
222
+ * @param enabledDexs - List of enabled DEX identifiers
223
+ * @param allowlistMarkets - List of allowed markets
224
+ * @param blocklistMarkets - List of blocked markets
225
+ */
226
+ async updateFeatureFlags(hip3Enabled, enabledDexs, allowlistMarkets, blocklistMarkets) {
227
+ const previousEnabledDexs = [...this.enabledDexs];
228
+ const previousAllowlistMarkets = [...this.allowlistMarkets];
229
+ const previousBlocklistMarkets = [...this.blocklistMarkets];
230
+ const previousHip3Enabled = this.hip3Enabled;
231
+ this.hip3Enabled = hip3Enabled;
232
+ this.enabledDexs = enabledDexs;
233
+ this.allowlistMarkets = allowlistMarkets;
234
+ this.blocklistMarkets = blocklistMarkets;
235
+ this.discoveredDexNames = enabledDexs; // Store DEX order for webData3 index mapping
236
+ // Resolve any pending DEX discovery wait now that DEXs are available
237
+ if (this.dexDiscoveryResolver && enabledDexs.length > 0) {
238
+ this.dexDiscoveryResolver();
239
+ this.dexDiscoveryPromise = null;
240
+ this.dexDiscoveryResolver = null;
241
+ }
242
+ this.deps.debugLogger.log('Feature flags updated:', {
243
+ previousHip3Enabled,
244
+ hip3Enabled,
245
+ previousEnabledDexs,
246
+ enabledDexs,
247
+ previousAllowlistMarkets,
248
+ allowlistMarkets,
249
+ previousBlocklistMarkets,
250
+ blocklistMarkets,
251
+ });
252
+ // If equity was just enabled or new DEXs were added
253
+ const newDexs = enabledDexs.filter((dex) => !previousEnabledDexs.includes(dex));
254
+ if ((!previousHip3Enabled && hip3Enabled && enabledDexs.length > 0) ||
255
+ newDexs.length > 0) {
256
+ this.deps.debugLogger.log('Establishing subscriptions for new DEXs:', newDexs);
257
+ // Establish assetCtxs subscriptions for new DEXs (for market data)
258
+ const hasMarketDataSubscribers = this.marketDataSubscribers.size > 0;
259
+ if (hasMarketDataSubscribers) {
260
+ await Promise.all(newDexs.map(async (dex) => {
261
+ try {
262
+ await this.ensureAssetCtxsSubscription(dex);
263
+ }
264
+ catch (assetCtxsError) {
265
+ this.deps.logger.error(ensureError(assetCtxsError), this.getErrorContext('updateFeatureFlags.ensureAssetCtxsSubscription', {
266
+ dex,
267
+ }));
268
+ }
269
+ }));
270
+ }
271
+ // Establish clearinghouseState/openOrders subscriptions for new DEXs
272
+ // (needed for positions, orders, and account data when using individual subscriptions)
273
+ const hasUserDataSubscribers = this.positionSubscriberCount > 0 ||
274
+ this.orderSubscriberCount > 0 ||
275
+ this.accountSubscriberCount > 0;
276
+ if (hasUserDataSubscribers && this.hip3Enabled) {
277
+ try {
278
+ const userAddress = await this.walletService.getUserAddressWithDefault();
279
+ await Promise.all(newDexs.map(async (dex) => {
280
+ try {
281
+ await this.ensureClearinghouseStateSubscription(userAddress, dex);
282
+ await this.ensureOpenOrdersSubscription(userAddress, dex);
283
+ this.deps.debugLogger.log(`Established user data subscriptions for new DEX: ${dex}`);
284
+ }
285
+ catch (userDataError) {
286
+ this.deps.logger.error(ensureError(userDataError), this.getErrorContext('updateFeatureFlags.ensureUserDataSubscription', { dex }));
287
+ }
288
+ }));
289
+ }
290
+ catch (addressError) {
291
+ this.deps.logger.error(ensureError(addressError), this.getErrorContext('updateFeatureFlags.getUserAddress'));
292
+ }
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * Fast hash function for change detection
298
+ * Uses string concatenation of key fields instead of JSON.stringify()
299
+ * Performance: ~100x faster than JSON.stringify() for typical objects
300
+ * Tracks structural changes (coin, size, entryPrice, leverage, TP/SL prices/counts)
301
+ * and value changes (unrealizedPnl, returnOnEquity) for live P&L updates
302
+ *
303
+ * @param positions - Positions to hash
304
+ * @returns Hash string
305
+ */
306
+ hashPositions(positions) {
307
+ if (!positions || positions.length === 0) {
308
+ return '0';
309
+ }
310
+ return positions
311
+ .map((pos) => `${pos.symbol}:${pos.size}:${pos.entryPrice}:${pos.leverage.value}:${pos.takeProfitPrice || ''}:${pos.stopLossPrice || ''}:${pos.takeProfitCount}:${pos.stopLossCount}:${pos.unrealizedPnl}:${pos.returnOnEquity}`)
312
+ .join('|');
313
+ }
314
+ /**
315
+ * Fast hash function for orders
316
+ *
317
+ * @param orders - Orders to hash
318
+ * @returns Hash string
319
+ */
320
+ hashOrders(orders) {
321
+ if (!orders || orders.length === 0) {
322
+ return '0';
323
+ }
324
+ return orders
325
+ .map((ord) => `${ord.symbol}:${ord.side}:${ord.size}:${ord.price}:${ord.orderType}`)
326
+ .join('|');
327
+ }
328
+ /**
329
+ * Fast hash function for account state
330
+ *
331
+ * @param account - Account state to hash
332
+ * @returns Hash string
333
+ */
334
+ hashAccountState(account) {
335
+ return `${account.availableBalance}:${account.totalBalance}:${account.marginUsed}:${account.unrealizedPnl}`;
336
+ }
337
+ /**
338
+ * Extract TP/SL from orders and optionally convert raw SDK orders to Order format.
339
+ * DRY helper used by both webData2 and clearinghouseState callbacks.
340
+ *
341
+ * @param orders - Raw SDK orders from WebSocket event
342
+ * @param positions - Current positions for TP/SL matching
343
+ * @param cachedProcessedOrders - Optional pre-processed orders (skips conversion if provided)
344
+ * @returns Maps for TP/SL prices and counts, plus processed Order array
345
+ */
346
+ extractTPSLFromOrders(orders, positions, cachedProcessedOrders) {
347
+ const tpslMap = new Map();
348
+ const tpslCountMap = new Map();
349
+ // If cached processed orders provided, extract TP/SL from them directly
350
+ if (cachedProcessedOrders) {
351
+ cachedProcessedOrders.forEach((order) => {
352
+ // Use triggerPrice for TP/SL (trigger condition price), falling back to price
353
+ // This ensures consistency with raw SDK order processing which uses triggerPx
354
+ const tpslPrice = order.triggerPrice || order.price;
355
+ if (order.isTrigger && tpslPrice) {
356
+ const isTakeProfit = order.detailedOrderType?.includes('Take Profit');
357
+ const isStop = order.detailedOrderType?.includes('Stop');
358
+ const matchingPosition = positions.find((pos) => pos.symbol === order.symbol);
359
+ // Determine TP vs SL classification for count and price updates
360
+ // Use order type first, fallback to price-based detection for ambiguous 'Trigger' types
361
+ let classifiedAsTakeProfit = isTakeProfit;
362
+ let classifiedAsStop = isStop;
363
+ if (!isTakeProfit && !isStop && matchingPosition) {
364
+ // Fallback: determine based on trigger price vs entry price
365
+ // This handles orders with ambiguous type 'Trigger'
366
+ const triggerPrice = parseFloat(tpslPrice);
367
+ const entryPrice = parseFloat(matchingPosition.entryPrice || '0');
368
+ const isLong = parseFloat(matchingPosition.size) > 0;
369
+ if (isLong) {
370
+ if (triggerPrice > entryPrice) {
371
+ classifiedAsTakeProfit = true;
372
+ }
373
+ else {
374
+ classifiedAsStop = true;
375
+ }
376
+ }
377
+ else if (triggerPrice < entryPrice) {
378
+ classifiedAsTakeProfit = true;
379
+ }
380
+ else {
381
+ classifiedAsStop = true;
382
+ }
383
+ }
384
+ const currentTakeProfitCount = tpslCountMap.get(order.symbol)?.takeProfitCount || 0;
385
+ const currentStopLossCount = tpslCountMap.get(order.symbol)?.stopLossCount || 0;
386
+ tpslCountMap.set(order.symbol, {
387
+ takeProfitCount: classifiedAsTakeProfit
388
+ ? currentTakeProfitCount + 1
389
+ : currentTakeProfitCount,
390
+ stopLossCount: classifiedAsStop
391
+ ? currentStopLossCount + 1
392
+ : currentStopLossCount,
393
+ });
394
+ if (matchingPosition) {
395
+ const existing = tpslMap.get(order.symbol) || {};
396
+ if (classifiedAsTakeProfit) {
397
+ existing.takeProfitPrice = tpslPrice;
398
+ }
399
+ else if (classifiedAsStop) {
400
+ existing.stopLossPrice = tpslPrice;
401
+ }
402
+ tpslMap.set(order.symbol, existing);
403
+ }
404
+ }
405
+ });
406
+ return { tpslMap, tpslCountMap, processedOrders: cachedProcessedOrders };
407
+ }
408
+ // Process raw SDK orders
409
+ const processedOrders = [];
410
+ orders.forEach((order) => {
411
+ let position;
412
+ let positionForCoin;
413
+ const matchPositionToTpsl = (pos) => {
414
+ if (perpsConfig_1.TP_SL_CONFIG.UsePositionBoundTpsl) {
415
+ return (pos.symbol === order.coin &&
416
+ order.reduceOnly &&
417
+ order.isPositionTpsl);
418
+ }
419
+ return (pos.symbol === order.coin &&
420
+ Math.abs(parseFloat(order.sz)) >= Math.abs(parseFloat(pos.size)));
421
+ };
422
+ const matchPositionToCoin = (pos) => pos.symbol === order.coin;
423
+ // Process trigger orders for TP/SL extraction
424
+ if (order.triggerPx) {
425
+ const isTakeProfit = order.orderType?.includes('Take Profit');
426
+ const isStop = order.orderType?.includes('Stop');
427
+ const { coin } = order;
428
+ position = positions.find(matchPositionToTpsl);
429
+ positionForCoin = positions.find(matchPositionToCoin);
430
+ // Determine TP vs SL classification for count and price updates
431
+ // Use order type first, fallback to price-based detection for ambiguous 'Trigger' types
432
+ // This matches the cached order processing logic for consistency
433
+ let classifiedAsTakeProfit = isTakeProfit;
434
+ let classifiedAsStop = isStop;
435
+ if (!isTakeProfit && !isStop && position) {
436
+ // Fallback: determine based on trigger price vs entry price
437
+ // This handles orders with ambiguous type 'Trigger'
438
+ const triggerPrice = parseFloat(order.triggerPx);
439
+ const entryPrice = parseFloat(position.entryPrice || '0');
440
+ const isLong = parseFloat(position.size) > 0;
441
+ if (isLong) {
442
+ if (triggerPrice > entryPrice) {
443
+ classifiedAsTakeProfit = true;
444
+ }
445
+ else {
446
+ classifiedAsStop = true;
447
+ }
448
+ }
449
+ else if (triggerPrice < entryPrice) {
450
+ classifiedAsTakeProfit = true;
451
+ }
452
+ else {
453
+ classifiedAsStop = true;
454
+ }
455
+ }
456
+ const currentTakeProfitCount = tpslCountMap.get(coin)?.takeProfitCount || 0;
457
+ const currentStopLossCount = tpslCountMap.get(coin)?.stopLossCount || 0;
458
+ tpslCountMap.set(coin, {
459
+ takeProfitCount: classifiedAsTakeProfit
460
+ ? currentTakeProfitCount + 1
461
+ : currentTakeProfitCount,
462
+ stopLossCount: classifiedAsStop
463
+ ? currentStopLossCount + 1
464
+ : currentStopLossCount,
465
+ });
466
+ if (position) {
467
+ const existing = tpslMap.get(coin) || {};
468
+ // Use classified values for price assignment (consistent with count logic)
469
+ if (classifiedAsTakeProfit) {
470
+ existing.takeProfitPrice = order.triggerPx;
471
+ }
472
+ else if (classifiedAsStop) {
473
+ existing.stopLossPrice = order.triggerPx;
474
+ }
475
+ tpslMap.set(coin, existing);
476
+ }
477
+ }
478
+ // Convert ALL open orders to Order format
479
+ const convertedOrder = (0, hyperLiquidAdapter_1.adaptOrderFromSDK)(order, position || positionForCoin);
480
+ processedOrders.push(convertedOrder);
481
+ });
482
+ return { tpslMap, tpslCountMap, processedOrders };
483
+ }
484
+ /**
485
+ * Merge TP/SL data into positions
486
+ * DRY helper used by both webData2 and clearinghouseState callbacks
487
+ *
488
+ * @param positions - Base positions without TP/SL
489
+ * @param tpslMap - Map of coin -> TP/SL prices
490
+ * @param tpslCountMap - Map of coin -> TP/SL counts
491
+ * @returns Positions enhanced with TP/SL data
492
+ */
493
+ mergeTPSLIntoPositions(positions, tpslMap, tpslCountMap) {
494
+ return positions.map((position) => {
495
+ const tpsl = tpslMap.get(position.symbol) || {};
496
+ const tpslCount = tpslCountMap.get(position.symbol) || {};
497
+ return {
498
+ ...position,
499
+ takeProfitPrice: tpsl.takeProfitPrice || undefined,
500
+ stopLossPrice: tpsl.stopLossPrice || undefined,
501
+ takeProfitCount: tpslCount.takeProfitCount || 0,
502
+ stopLossCount: tpslCount.stopLossCount || 0,
503
+ };
504
+ });
505
+ }
506
+ /**
507
+ * Aggregate account states from all cached DEXs
508
+ * Sums balances and creates per-DEX breakdown for multi-DEX portfolio view
509
+ *
510
+ * @returns Aggregated account state with dexBreakdown field
511
+ */
512
+ aggregateAccountStates() {
513
+ const subAccountBreakdown = {};
514
+ let totalAvailableBalance = 0;
515
+ let totalBalance = 0;
516
+ let totalMarginUsed = 0;
517
+ let totalUnrealizedPnl = 0;
518
+ // Collect account states for weighted ROE calculation
519
+ const accountStatesForROE = [];
520
+ // Aggregate all cached account states
521
+ Array.from(this.dexAccountCache.entries()).forEach(([currentDex, state]) => {
522
+ const dexKey = currentDex === '' ? 'main' : currentDex;
523
+ subAccountBreakdown[dexKey] = {
524
+ availableBalance: state.availableBalance,
525
+ totalBalance: state.totalBalance,
526
+ };
527
+ totalAvailableBalance += parseFloat(state.availableBalance);
528
+ totalBalance += parseFloat(state.totalBalance);
529
+ totalMarginUsed += parseFloat(state.marginUsed);
530
+ totalUnrealizedPnl += parseFloat(state.unrealizedPnl);
531
+ // Collect data for weighted ROE calculation
532
+ accountStatesForROE.push({
533
+ unrealizedPnl: state.unrealizedPnl,
534
+ returnOnEquity: state.returnOnEquity,
535
+ });
536
+ });
537
+ // Use first DEX's account state as base and override aggregated values
538
+ const firstDexAccount = this.dexAccountCache.values().next().value || {};
539
+ // Calculate weighted returnOnEquity across all DEXs
540
+ const returnOnEquity = (0, accountUtils_1.calculateWeightedReturnOnEquity)(accountStatesForROE);
541
+ return {
542
+ ...firstDexAccount,
543
+ availableBalance: totalAvailableBalance.toString(),
544
+ totalBalance: totalBalance.toString(),
545
+ marginUsed: totalMarginUsed.toString(),
546
+ unrealizedPnl: totalUnrealizedPnl.toString(),
547
+ subAccountBreakdown,
548
+ returnOnEquity,
549
+ };
550
+ }
551
+ /**
552
+ * Subscribe to live price updates with singleton subscription architecture
553
+ * Uses allMids for fast price updates and predictedFundings for accurate funding rates
554
+ *
555
+ * @param params - Subscription parameters
556
+ * @returns Cleanup function
557
+ */
558
+ async subscribeToPrices(params) {
559
+ const { symbols, callback, includeOrderBook = false, includeMarketData = false, } = params;
560
+ const unsubscribers = [];
561
+ symbols.forEach((symbol) => {
562
+ unsubscribers.push(this.createSubscription(this.priceSubscribers, callback, symbol));
563
+ // Track market data subscribers separately
564
+ if (includeMarketData) {
565
+ unsubscribers.push(this.createSubscription(this.marketDataSubscribers, callback, symbol));
566
+ }
567
+ // Track order book subscribers separately
568
+ if (includeOrderBook) {
569
+ unsubscribers.push(this.createSubscription(this.orderBookSubscribers, callback, symbol));
570
+ }
571
+ });
572
+ await this.clientService.ensureSubscriptionClient(this.walletService.createWalletAdapter());
573
+ const subscriptionClient = this.clientService.getSubscriptionClient();
574
+ if (!subscriptionClient) {
575
+ this.deps.debugLogger.log('SubscriptionClient not available for price subscription');
576
+ return () => unsubscribers.forEach((fn) => fn());
577
+ }
578
+ // Ensure global subscriptions are established
579
+ this.ensureGlobalAllMidsSubscription();
580
+ // HIP-3: Establish assetCtxs subscriptions ONLY for DEXs with requested symbols
581
+ // Performance: Avoid unnecessary WebSocket connections for unused DEXs
582
+ if (includeMarketData) {
583
+ // Extract unique DEXs from requested symbols
584
+ const dexsNeeded = new Set();
585
+ symbols.forEach((symbol) => {
586
+ const { dex } = (0, hyperLiquidAdapter_1.parseAssetName)(symbol);
587
+ dexsNeeded.add(dex);
588
+ });
589
+ // Only subscribe to DEXs that have requested symbols
590
+ dexsNeeded.forEach((dex) => {
591
+ const dexName = dex ?? '';
592
+ this.ensureAssetCtxsSubscription(dexName).catch((assetCtxsError) => {
593
+ this.deps.logger.error(ensureError(assetCtxsError), this.getErrorContext('subscribeToPrices.ensureAssetCtxsSubscription', { dex: dexName }));
594
+ });
595
+ });
596
+ }
597
+ // Note: Funding rates are now cached via assetCtxs WebSocket subscription
598
+ // (ensureAssetCtxsSubscription above), eliminating the need for a separate
599
+ // metaAndAssetCtxs API call here. The WebSocket callback in createAssetCtxsSubscription
600
+ // populates marketDataCache with funding rates as they arrive.
601
+ symbols.forEach((symbol) => {
602
+ // Subscribe to activeAssetCtx only when market data is requested
603
+ if (includeMarketData) {
604
+ this.ensureActiveAssetSubscription(symbol);
605
+ }
606
+ if (includeOrderBook) {
607
+ this.ensureBboSubscription(symbol);
608
+ }
609
+ });
610
+ // Send cached data immediately if available
611
+ symbols.forEach((symbol) => {
612
+ const cachedPrice = this.cachedPriceData?.get(symbol);
613
+ if (cachedPrice) {
614
+ callback([cachedPrice]);
615
+ }
616
+ });
617
+ // Return cleanup function
618
+ return () => {
619
+ unsubscribers.forEach((fn) => fn());
620
+ // Cleanup subscriptions with reference counting
621
+ symbols.forEach((symbol) => {
622
+ if (includeMarketData) {
623
+ this.cleanupActiveAssetSubscription(symbol);
624
+ }
625
+ if (includeOrderBook) {
626
+ this.cleanupBboSubscription(symbol);
627
+ }
628
+ });
629
+ // Cleanup DEX-level assetCtxs subscriptions
630
+ if (includeMarketData) {
631
+ // Extract unique DEXs from requested symbols
632
+ const dexsNeeded = new Set();
633
+ symbols.forEach((symbol) => {
634
+ const { dex } = (0, hyperLiquidAdapter_1.parseAssetName)(symbol);
635
+ dexsNeeded.add(dex);
636
+ });
637
+ // Cleanup assetCtxs subscription for each DEX
638
+ dexsNeeded.forEach((dex) => {
639
+ const dexName = dex ?? '';
640
+ this.cleanupAssetCtxsSubscription(dexName);
641
+ });
642
+ }
643
+ };
644
+ }
645
+ /**
646
+ * Ensure shared webData3 subscription is active (singleton pattern with multi-DEX support)
647
+ * webData3 provides data for all DEXs (main + HIP-3) in a single subscription
648
+ *
649
+ * @param accountId - Optional account ID
650
+ */
651
+ async ensureSharedWebData3Subscription(accountId) {
652
+ // Establish webData3 subscription (if not exists)
653
+ if (!this.webData3Subscriptions.has('')) {
654
+ if (!this.webData3SubscriptionPromise) {
655
+ this.webData3SubscriptionPromise =
656
+ this.createUserDataSubscription(accountId);
657
+ try {
658
+ await this.webData3SubscriptionPromise;
659
+ }
660
+ catch {
661
+ this.webData3SubscriptionPromise = undefined;
662
+ throw new Error('Failed to create user data subscription');
663
+ }
664
+ }
665
+ else {
666
+ await this.webData3SubscriptionPromise;
667
+ }
668
+ }
669
+ // Note: webData3 includes all DEX data, so no separate HIP-3 subscriptions needed
670
+ }
671
+ /**
672
+ * Create WebSocket subscription for user data (positions, orders, account)
673
+ * - Uses webData2 when HIP-3 disabled (main DEX only)
674
+ * - Uses webData3 when HIP-3 enabled (main + HIP-3 DEXs)
675
+ *
676
+ * webData2 provides data for main DEX only
677
+ * webData3 provides perpDexStates[] array containing data for all DEXs:
678
+ * - Index 0: Main DEX (dexName = '')
679
+ * - Index 1+: HIP-3 DEXs in order of enabledDexs array
680
+ *
681
+ * @param accountId - Optional account ID
682
+ */
683
+ async createUserDataSubscription(accountId) {
684
+ await this.clientService.ensureSubscriptionClient(this.walletService.createWalletAdapter());
685
+ const subscriptionClient = this.clientService.getSubscriptionClient();
686
+ if (!subscriptionClient) {
687
+ throw new Error('Subscription client not initialized');
688
+ }
689
+ const userAddress = await this.walletService.getUserAddressWithDefault(accountId);
690
+ const dexName = ''; // Use empty string as key for single subscription
691
+ // Skip if subscription already exists
692
+ if (this.webData3Subscriptions.has(dexName)) {
693
+ return;
694
+ }
695
+ // Wait for DEX discovery if HIP-3 is enabled but DEXs haven't been discovered yet
696
+ // This ensures HIP-3 subscriptions are created together with main DEX
697
+ if (this.hip3Enabled && this.enabledDexs.length === 0) {
698
+ this.deps.debugLogger.log('Waiting for DEX discovery before creating subscriptions...');
699
+ await this.waitForDexDiscovery();
700
+ this.deps.debugLogger.log('DEX discovery complete, proceeding with subscriptions', {
701
+ enabledDexs: this.enabledDexs,
702
+ });
703
+ }
704
+ return new Promise((resolve, reject) => {
705
+ // Choose channel based on HIP-3 master switch
706
+ if (!this.hip3Enabled) {
707
+ // HIP-3 disabled: Use webData2 (main DEX only)
708
+ subscriptionClient
709
+ .webData2({ user: userAddress }, (data) => {
710
+ try {
711
+ // webData2 returns clearinghouseState for main DEX only
712
+ const currentDexName = ''; // Main DEX
713
+ // Check for removed fields before accessing
714
+ if (!data.clearinghouseState) {
715
+ return;
716
+ }
717
+ // Extract and process positions from clearinghouseState
718
+ const positions = data.clearinghouseState.assetPositions
719
+ .filter((assetPos) => assetPos.position.szi !== '0')
720
+ .map((assetPos) => (0, hyperLiquidAdapter_1.adaptPositionFromSDK)(assetPos));
721
+ // Extract TP/SL from orders
722
+ const { tpslMap, tpslCountMap, processedOrders: processedOrdersData, } = this.extractTPSLFromOrders(data.openOrders || [], positions);
723
+ // Merge TP/SL data into positions
724
+ const positionsWithTPSL = this.mergeTPSLIntoPositions(positions, tpslMap, tpslCountMap);
725
+ // Extract account data (webData2 provides clearinghouseState)
726
+ const accountState = (0, hyperLiquidAdapter_1.adaptAccountStateFromSDK)(data.clearinghouseState, undefined);
727
+ // Store in caches (main DEX only)
728
+ this.dexPositionsCache.set(currentDexName, positionsWithTPSL);
729
+ this.dexOrdersCache.set(currentDexName, processedOrdersData);
730
+ this.dexAccountCache.set(currentDexName, accountState);
731
+ // OI caps (main DEX only)
732
+ const oiCaps = data.perpsAtOpenInterestCap || [];
733
+ const oiCapsHash = [...oiCaps]
734
+ .sort((a, b) => a.localeCompare(b))
735
+ .join(',');
736
+ if (oiCapsHash !== this.cachedOICapsHash) {
737
+ this.cachedOICaps = oiCaps;
738
+ this.cachedOICapsHash = oiCapsHash;
739
+ this.oiCapsCacheInitialized = true;
740
+ this.oiCapSubscribers.forEach((oiCallback) => oiCallback(oiCaps));
741
+ }
742
+ // Notify subscribers (no aggregation needed - only main DEX)
743
+ const positionsHash = this.hashPositions(positionsWithTPSL);
744
+ const ordersHash = this.hashOrders(processedOrdersData);
745
+ const accountHash = this.hashAccountState(accountState);
746
+ if (positionsHash !== this.cachedPositionsHash) {
747
+ this.cachedPositions = positionsWithTPSL;
748
+ this.cachedPositionsHash = positionsHash;
749
+ this.positionsCacheInitialized = true;
750
+ this.positionSubscribers.forEach((posCallback) => posCallback(positionsWithTPSL));
751
+ }
752
+ if (ordersHash !== this.cachedOrdersHash) {
753
+ this.cachedOrders = processedOrdersData;
754
+ this.cachedOrdersHash = ordersHash;
755
+ this.ordersCacheInitialized = true;
756
+ this.orderSubscribers.forEach((ordCallback) => ordCallback(processedOrdersData));
757
+ }
758
+ if (accountHash !== this.cachedAccountHash) {
759
+ this.cachedAccount = accountState;
760
+ this.cachedAccountHash = accountHash;
761
+ this.accountSubscribers.forEach((accCallback) => accCallback(accountState));
762
+ }
763
+ }
764
+ catch (callbackError) {
765
+ this.deps.logger.error(ensureError(callbackError), this.getErrorContext('webData2 callback error', {
766
+ user: userAddress,
767
+ dataKeys: data ? Object.keys(data) : 'data is null/undefined',
768
+ hasClearinghouseState: data?.clearinghouseState !== undefined,
769
+ hasOpenOrders: data?.openOrders !== undefined,
770
+ hasPerpsAtOpenInterestCap: data?.perpsAtOpenInterestCap !== undefined,
771
+ }));
772
+ }
773
+ })
774
+ .then((subscription) => {
775
+ this.webData3Subscriptions.set(dexName, subscription);
776
+ this.deps.debugLogger.log('webData2 subscription established for main DEX only');
777
+ resolve();
778
+ })
779
+ .catch((subscribeError) => {
780
+ this.deps.logger.error(ensureError(subscribeError), this.getErrorContext('createUserDataSubscription (webData2)', {
781
+ dex: dexName,
782
+ }));
783
+ reject(ensureError(subscribeError));
784
+ });
785
+ }
786
+ else {
787
+ // HIP-3 enabled: Use individual subscriptions for positions/orders/account
788
+ // webData3 is only used for OI caps extraction
789
+ // Determine which DEXs to subscribe to
790
+ const dexsToSubscribe = [
791
+ '', // Main DEX
792
+ ...this.enabledDexs.filter((dex) => this.isDexEnabled(dex)),
793
+ ];
794
+ // Track expected DEXs for synchronized notifications
795
+ // Clear previous tracking and set new expected DEXs
796
+ this.expectedDexs = new Set(dexsToSubscribe);
797
+ this.initializedDexs = new Set();
798
+ // Set up individual subscriptions for each DEX
799
+ const subscriptionPromises = [];
800
+ for (const currentDexName of dexsToSubscribe) {
801
+ // Set up clearinghouseState subscription for positions + account
802
+ subscriptionPromises.push(this.ensureClearinghouseStateSubscription(userAddress, currentDexName));
803
+ // Set up openOrders subscription for orders
804
+ subscriptionPromises.push(this.ensureOpenOrdersSubscription(userAddress, currentDexName));
805
+ }
806
+ // Also set up webData3 for OI caps only
807
+ const webData3Promise = subscriptionClient
808
+ .webData3({ user: userAddress }, (data) => {
809
+ try {
810
+ // webData3 is ONLY used for OI caps extraction
811
+ // Positions, orders, and account data come from individual subscriptions
812
+ const allOICaps = [];
813
+ data.perpDexStates.forEach((dexState, index) => {
814
+ // Map webData3 index to DEX name
815
+ // Index 0 = main DEX (null), Index 1+ = HIP-3 DEXs from discoveredDexNames
816
+ const dexIdentifier = index === 0 ? null : this.discoveredDexNames[index - 1];
817
+ // Skip unknown DEXs (not in discoveredDexNames) to prevent main DEX cache corruption
818
+ if (index > 0 && dexIdentifier === undefined) {
819
+ return; // Unknown DEX - skip to prevent misidentifying as main DEX
820
+ }
821
+ // Only process DEXs we care about (skip others silently)
822
+ if (!this.isDexEnabled(dexIdentifier ?? null)) {
823
+ return; // Skip this DEX - not enabled in our configuration
824
+ }
825
+ const currentDexNameInLoop = dexIdentifier ?? '';
826
+ const oiCaps = dexState.perpsAtOpenInterestCap || [];
827
+ // Add DEX prefix for HIP-3 symbols (e.g., "xyz:TSLA")
828
+ if (currentDexNameInLoop) {
829
+ allOICaps.push(...oiCaps.map((symbolName) => `${currentDexNameInLoop}:${symbolName}`));
830
+ }
831
+ else {
832
+ // Main DEX - no prefix needed
833
+ allOICaps.push(...oiCaps);
834
+ }
835
+ });
836
+ // Update OI caps cache and notify if changed
837
+ const oiCapsHash = [...allOICaps]
838
+ .sort((a, b) => a.localeCompare(b))
839
+ .join(',');
840
+ if (oiCapsHash !== this.cachedOICapsHash) {
841
+ this.cachedOICaps = allOICaps;
842
+ this.cachedOICapsHash = oiCapsHash;
843
+ this.oiCapsCacheInitialized = true;
844
+ // Notify all subscribers
845
+ this.oiCapSubscribers.forEach((oiCallback) => oiCallback(allOICaps));
846
+ }
847
+ }
848
+ catch (callbackError) {
849
+ this.deps.logger.error(ensureError(callbackError), this.getErrorContext('webData3 callback error', {
850
+ user: userAddress,
851
+ hasPerpDexStates: data?.perpDexStates !== undefined,
852
+ perpDexStatesLength: data?.perpDexStates?.length ?? 0,
853
+ }));
854
+ }
855
+ })
856
+ .then((sub) => {
857
+ this.webData3Subscriptions.set(dexName, sub);
858
+ this.deps.debugLogger.log(`webData3 subscription established for OI caps (main + HIP-3)`);
859
+ })
860
+ .catch((subscribeError) => {
861
+ this.deps.logger.error(ensureError(subscribeError), this.getErrorContext('createUserDataSubscription (webData3)', {
862
+ dex: dexName,
863
+ }));
864
+ throw subscribeError;
865
+ });
866
+ subscriptionPromises.push(webData3Promise);
867
+ // Wait for all subscriptions to be established
868
+ Promise.all(subscriptionPromises)
869
+ .then(() => {
870
+ this.deps.debugLogger.log(`HIP-3 user data subscriptions established for ${dexsToSubscribe.length} DEXs`);
871
+ resolve();
872
+ })
873
+ .catch((allError) => {
874
+ this.deps.logger.error(ensureError(allError), this.getErrorContext('createUserDataSubscription (HIP-3)', {
875
+ dexs: dexsToSubscribe,
876
+ }));
877
+ reject(ensureError(allError));
878
+ });
879
+ }
880
+ });
881
+ }
882
+ /**
883
+ * Ensure clearinghouseState subscription exists for a DEX
884
+ *
885
+ * @param userAddress - User address
886
+ * @param dexName - DEX name ('' for main)
887
+ */
888
+ async ensureClearinghouseStateSubscription(userAddress, dexName) {
889
+ if (this.clearinghouseStateSubscriptions.has(dexName)) {
890
+ return; // Already subscribed
891
+ }
892
+ const subscriptionClient = this.clientService.getSubscriptionClient();
893
+ if (!subscriptionClient) {
894
+ throw new Error('Subscription client not available');
895
+ }
896
+ try {
897
+ const subscription = await subscriptionClient.clearinghouseState({
898
+ user: userAddress,
899
+ dex: dexName || undefined, // Empty string -> undefined for main DEX
900
+ }, (data) => {
901
+ const cacheKey = data.dex || '';
902
+ // Update caches and notify subscribers if we have positions/account subscribers
903
+ if (this.positionSubscriberCount > 0 ||
904
+ this.accountSubscriberCount > 0) {
905
+ // Process positions from clearinghouse state
906
+ const positions = data.clearinghouseState.assetPositions
907
+ .filter((assetPos) => assetPos.position.szi !== '0')
908
+ .map((assetPos) => (0, hyperLiquidAdapter_1.adaptPositionFromSDK)(assetPos));
909
+ // Get cached orders to preserve TP/SL data (prevents flickering)
910
+ // Orders are cached by openOrders subscription
911
+ const cachedOrdersForDex = this.dexOrdersCache.get(cacheKey) || [];
912
+ // Re-extract TP/SL from cached orders for the new positions
913
+ // This ensures TP/SL data persists across clearinghouseState updates
914
+ let positionsWithTPSL = positions;
915
+ if (cachedOrdersForDex.length > 0) {
916
+ const { tpslMap, tpslCountMap } = this.extractTPSLFromOrders([], positions, cachedOrdersForDex);
917
+ positionsWithTPSL = this.mergeTPSLIntoPositions(positions, tpslMap, tpslCountMap);
918
+ }
919
+ // Update account state
920
+ const accountState = (0, hyperLiquidAdapter_1.adaptAccountStateFromSDK)(data.clearinghouseState, undefined);
921
+ // Update caches
922
+ this.dexPositionsCache.set(cacheKey, positionsWithTPSL);
923
+ this.dexAccountCache.set(cacheKey, accountState);
924
+ // Mark this DEX as initialized (has sent first data)
925
+ this.initializedDexs.add(cacheKey);
926
+ // Trigger aggregation and notify subscribers
927
+ this.aggregateAndNotifySubscribers();
928
+ }
929
+ });
930
+ this.clearinghouseStateSubscriptions.set(dexName, subscription);
931
+ this.deps.debugLogger.log(`clearinghouseState subscription established for DEX: ${dexName || 'main'}`);
932
+ }
933
+ catch (subscribeError) {
934
+ // Remove this DEX from expected set so it doesn't block notifications for other DEXs
935
+ this.expectedDexs.delete(dexName);
936
+ this.deps.logger.error(ensureError(subscribeError), this.getErrorContext('ensureClearinghouseStateSubscription', {
937
+ dex: dexName,
938
+ }));
939
+ throw subscribeError;
940
+ }
941
+ }
942
+ /**
943
+ * Ensure openOrders subscription exists for a DEX
944
+ *
945
+ * @param userAddress - User address
946
+ * @param dexName - DEX name ('' for main)
947
+ */
948
+ async ensureOpenOrdersSubscription(userAddress, dexName) {
949
+ if (this.openOrdersSubscriptions.has(dexName)) {
950
+ return; // Already subscribed
951
+ }
952
+ const subscriptionClient = this.clientService.getSubscriptionClient();
953
+ if (!subscriptionClient) {
954
+ throw new Error('Subscription client not available');
955
+ }
956
+ try {
957
+ const subscription = await subscriptionClient.openOrders({
958
+ user: userAddress,
959
+ dex: dexName || undefined, // Empty string -> undefined for main DEX
960
+ }, (data) => {
961
+ const cacheKey = data.dex || '';
962
+ // Update caches and notify subscribers if we have order subscribers
963
+ if (this.orderSubscriberCount > 0 ||
964
+ this.positionSubscriberCount > 0) {
965
+ // Get cached positions for TP/SL processing
966
+ const cachedPositionsForDex = this.dexPositionsCache.get(cacheKey) || [];
967
+ // Extract TP/SL and process orders
968
+ const { tpslMap, tpslCountMap, processedOrders: ordersProcessed, } = this.extractTPSLFromOrders(data.orders, cachedPositionsForDex);
969
+ // Update orders cache with processed orders
970
+ this.dexOrdersCache.set(cacheKey, ordersProcessed);
971
+ // Update positions with TP/SL if we have positions
972
+ if (cachedPositionsForDex.length > 0) {
973
+ const positionsWithTPSL = this.mergeTPSLIntoPositions(cachedPositionsForDex, tpslMap, tpslCountMap);
974
+ this.dexPositionsCache.set(cacheKey, positionsWithTPSL);
975
+ }
976
+ // Mark this DEX as initialized (has sent first data)
977
+ this.initializedDexs.add(cacheKey);
978
+ // Trigger aggregation and notify subscribers
979
+ this.aggregateAndNotifySubscribers();
980
+ }
981
+ });
982
+ this.openOrdersSubscriptions.set(dexName, subscription);
983
+ this.deps.debugLogger.log(`openOrders subscription established for DEX: ${dexName || 'main'}`);
984
+ }
985
+ catch (subscribeError) {
986
+ // Remove this DEX from expected set so it doesn't block notifications for other DEXs
987
+ this.expectedDexs.delete(dexName);
988
+ this.deps.logger.error(ensureError(subscribeError), this.getErrorContext('ensureOpenOrdersSubscription', {
989
+ dex: dexName,
990
+ }));
991
+ throw subscribeError;
992
+ }
993
+ }
994
+ /**
995
+ * Aggregate data from all DEX caches and notify subscribers if data changed
996
+ * Used by both webData3 callback and fallback subscription callbacks
997
+ */
998
+ aggregateAndNotifySubscribers() {
999
+ // Wait for all expected DEXs to send initial data before notifying
1000
+ // This ensures positions from all DEXs appear simultaneously
1001
+ if (this.expectedDexs.size > 0) {
1002
+ const allDexsInitialized = Array.from(this.expectedDexs).every((dex) => this.initializedDexs.has(dex));
1003
+ if (!allDexsInitialized) {
1004
+ this.deps.debugLogger.log('Waiting for all DEXs to send initial data', {
1005
+ expected: Array.from(this.expectedDexs),
1006
+ initialized: Array.from(this.initializedDexs),
1007
+ });
1008
+ return; // Don't notify yet - waiting for more DEXs
1009
+ }
1010
+ }
1011
+ // Aggregate data from all DEX caches
1012
+ // Order: Main DEX (crypto perps) first, then HIP-3 DEXs
1013
+ const mainDexPositions = this.dexPositionsCache.get('') || [];
1014
+ const hip3DexPositions = Array.from(this.dexPositionsCache.entries())
1015
+ .filter(([key]) => key !== '')
1016
+ .flatMap(([, positionsForDex]) => positionsForDex);
1017
+ const aggregatedPositions = [...mainDexPositions, ...hip3DexPositions];
1018
+ const mainDexOrders = this.dexOrdersCache.get('') || [];
1019
+ const hip3DexOrders = Array.from(this.dexOrdersCache.entries())
1020
+ .filter(([key]) => key !== '')
1021
+ .flatMap(([, ordersForDex]) => ordersForDex);
1022
+ const aggregatedOrders = [...mainDexOrders, ...hip3DexOrders];
1023
+ const aggregatedAccount = this.aggregateAccountStates();
1024
+ // Check if aggregated data changed using fast hash comparison
1025
+ const positionsHash = this.hashPositions(aggregatedPositions);
1026
+ const ordersHash = this.hashOrders(aggregatedOrders);
1027
+ const accountHash = this.hashAccountState(aggregatedAccount);
1028
+ const positionsChanged = positionsHash !== this.cachedPositionsHash;
1029
+ const ordersChanged = ordersHash !== this.cachedOrdersHash;
1030
+ const accountChanged = accountHash !== this.cachedAccountHash;
1031
+ // Only notify subscribers if aggregated data changed
1032
+ if (positionsChanged) {
1033
+ this.cachedPositions = aggregatedPositions;
1034
+ this.cachedPositionsHash = positionsHash;
1035
+ this.positionsCacheInitialized = true; // Mark cache as initialized
1036
+ this.positionSubscribers.forEach((posCallback) => {
1037
+ posCallback(aggregatedPositions);
1038
+ });
1039
+ }
1040
+ if (ordersChanged) {
1041
+ this.cachedOrders = aggregatedOrders;
1042
+ this.cachedOrdersHash = ordersHash;
1043
+ this.ordersCacheInitialized = true; // Mark cache as initialized
1044
+ this.orderSubscribers.forEach((ordCallback) => {
1045
+ ordCallback(aggregatedOrders);
1046
+ });
1047
+ }
1048
+ if (accountChanged) {
1049
+ this.cachedAccount = aggregatedAccount;
1050
+ this.cachedAccountHash = accountHash;
1051
+ this.accountSubscribers.forEach((accCallback) => {
1052
+ accCallback(aggregatedAccount);
1053
+ });
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Clean up webData3 subscription when no longer needed
1058
+ */
1059
+ cleanupSharedWebData3ISubscription() {
1060
+ const totalSubscribers = this.positionSubscriberCount +
1061
+ this.orderSubscriberCount +
1062
+ this.accountSubscriberCount +
1063
+ this.oiCapSubscriberCount;
1064
+ if (totalSubscribers <= 0) {
1065
+ // Cleanup webData3 subscription (covers all DEXs)
1066
+ if (this.webData3Subscriptions.size > 0) {
1067
+ this.webData3Subscriptions.forEach((subscription, dexNameKey) => {
1068
+ subscription.unsubscribe().catch((unsubError) => {
1069
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('cleanupSharedWebData3ISubscription.webData3', {
1070
+ dex: dexNameKey,
1071
+ }));
1072
+ });
1073
+ });
1074
+ this.webData3Subscriptions.clear();
1075
+ this.webData3SubscriptionPromise = undefined;
1076
+ }
1077
+ // Cleanup individual subscriptions (clearinghouseState + openOrders)
1078
+ if (this.clearinghouseStateSubscriptions.size > 0) {
1079
+ this.clearinghouseStateSubscriptions.forEach((subscription, dexNameKey) => {
1080
+ subscription.unsubscribe().catch((unsubError) => {
1081
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('cleanupSharedWebData3ISubscription.clearinghouseState', {
1082
+ dex: dexNameKey,
1083
+ }));
1084
+ });
1085
+ });
1086
+ this.clearinghouseStateSubscriptions.clear();
1087
+ }
1088
+ if (this.openOrdersSubscriptions.size > 0) {
1089
+ this.openOrdersSubscriptions.forEach((subscription, dexNameKey) => {
1090
+ subscription.unsubscribe().catch((unsubError) => {
1091
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('cleanupSharedWebData3ISubscription.openOrders', {
1092
+ dex: dexNameKey,
1093
+ }));
1094
+ });
1095
+ });
1096
+ this.openOrdersSubscriptions.clear();
1097
+ }
1098
+ // Clear subscriber counts
1099
+ this.positionSubscriberCount = 0;
1100
+ this.orderSubscriberCount = 0;
1101
+ this.accountSubscriberCount = 0;
1102
+ this.oiCapSubscriberCount = 0;
1103
+ // Clear per-DEX caches
1104
+ this.dexPositionsCache.clear();
1105
+ this.dexOrdersCache.clear();
1106
+ this.dexAccountCache.clear();
1107
+ // Clear DEX tracking for synchronized notifications
1108
+ this.expectedDexs.clear();
1109
+ this.initializedDexs.clear();
1110
+ // Clear aggregated caches
1111
+ this.cachedPositions = null;
1112
+ this.cachedOrders = null;
1113
+ this.cachedAccount = null;
1114
+ this.ordersCacheInitialized = false; // Reset cache initialization flag
1115
+ this.positionsCacheInitialized = false; // Reset cache initialization flag
1116
+ // Clear hash caches
1117
+ this.cachedPositionsHash = '';
1118
+ this.cachedOrdersHash = '';
1119
+ this.cachedAccountHash = '';
1120
+ this.deps.debugLogger.log('All multi-DEX subscriptions cleaned up (webData2/3 + individual subscriptions)');
1121
+ }
1122
+ }
1123
+ /**
1124
+ * Subscribe to live position updates with TP/SL data
1125
+ *
1126
+ * @param params - Subscription parameters
1127
+ * @returns Cleanup function
1128
+ */
1129
+ subscribeToPositions(params) {
1130
+ const { callback, accountId } = params;
1131
+ const unsubscribe = this.createSubscription(this.positionSubscribers, callback);
1132
+ // Increment position subscriber count
1133
+ this.positionSubscriberCount++;
1134
+ // Immediately provide cached data if available
1135
+ if (this.cachedPositions) {
1136
+ callback(this.cachedPositions);
1137
+ }
1138
+ // Ensure shared subscription is active
1139
+ this.ensureSharedWebData3Subscription(accountId).catch((subError) => {
1140
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('subscribeToPositions'));
1141
+ });
1142
+ return () => {
1143
+ unsubscribe();
1144
+ this.positionSubscriberCount--;
1145
+ this.cleanupSharedWebData3ISubscription();
1146
+ };
1147
+ }
1148
+ /**
1149
+ * Subscribe to open interest cap updates
1150
+ * OI caps are extracted from webData2 subscription (zero additional overhead)
1151
+ *
1152
+ * @param params - Subscription parameters
1153
+ * @returns Cleanup function
1154
+ */
1155
+ subscribeToOICaps(params) {
1156
+ const { callback, accountId } = params;
1157
+ // Create subscription
1158
+ const unsubscribe = this.createSubscription(this.oiCapSubscribers, callback);
1159
+ // Increment OI cap subscriber count
1160
+ this.oiCapSubscriberCount++;
1161
+ // Immediately provide cached data if available
1162
+ if (this.cachedOICaps) {
1163
+ callback(this.cachedOICaps);
1164
+ }
1165
+ // Ensure webData3 subscription is active (OI caps come from webData3)
1166
+ this.ensureSharedWebData3Subscription(accountId).catch((subError) => {
1167
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('subscribeToOICaps'));
1168
+ });
1169
+ return () => {
1170
+ unsubscribe();
1171
+ this.oiCapSubscriberCount--;
1172
+ this.cleanupSharedWebData3ISubscription();
1173
+ };
1174
+ }
1175
+ /**
1176
+ * Check if OI caps cache has been initialized
1177
+ * Useful for preventing UI flashing before first data arrives
1178
+ *
1179
+ * @returns True if cache is initialized
1180
+ */
1181
+ isOICapsCacheInitialized() {
1182
+ return this.oiCapsCacheInitialized;
1183
+ }
1184
+ /**
1185
+ * Subscribe to live order fill updates
1186
+ * Shares subscriptions per accountId to avoid duplicate WebSocket connections
1187
+ *
1188
+ * @param params - Subscription parameters
1189
+ * @returns Cleanup function
1190
+ */
1191
+ subscribeToOrderFills(params) {
1192
+ const { callback, accountId } = params;
1193
+ // Normalize accountId: undefined -> 'default' for Map key
1194
+ const normalizedAccountId = accountId ?? 'default';
1195
+ const unsubscribe = this.createSubscription(this.orderFillSubscribers, callback, normalizedAccountId);
1196
+ // Ensure subscription is established for this accountId
1197
+ this.ensureOrderFillISubscription(accountId).catch((subError) => {
1198
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('subscribeToOrderFills'));
1199
+ });
1200
+ return () => {
1201
+ unsubscribe();
1202
+ // If no more subscribers for this accountId, clean up subscription
1203
+ const subscribers = this.orderFillSubscribers.get(normalizedAccountId);
1204
+ if (!subscribers || subscribers.size === 0) {
1205
+ const subscription = this.orderFillSubscriptions.get(normalizedAccountId);
1206
+ if (subscription) {
1207
+ subscription.unsubscribe().catch((unsubError) => {
1208
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('subscribeToOrderFills.unsubscribe'));
1209
+ });
1210
+ this.orderFillSubscriptions.delete(normalizedAccountId);
1211
+ }
1212
+ }
1213
+ };
1214
+ }
1215
+ /**
1216
+ * Ensure order fill subscription is active for the given accountId
1217
+ * Shares subscription across all callbacks for the same accountId
1218
+ *
1219
+ * @param accountId - Optional account ID
1220
+ */
1221
+ async ensureOrderFillISubscription(accountId) {
1222
+ // Normalize accountId: undefined -> 'default' for Map key
1223
+ const normalizedAccountId = accountId ?? 'default';
1224
+ // If subscription already exists, no need to create another
1225
+ if (this.orderFillSubscriptions.has(normalizedAccountId)) {
1226
+ return;
1227
+ }
1228
+ const subscriptionClient = this.clientService.getSubscriptionClient();
1229
+ if (!subscriptionClient) {
1230
+ await this.clientService.ensureSubscriptionClient(this.walletService.createWalletAdapter());
1231
+ const client = this.clientService.getSubscriptionClient();
1232
+ if (!client) {
1233
+ throw new Error('SubscriptionClient not available');
1234
+ }
1235
+ return this.ensureOrderFillISubscription(accountId);
1236
+ }
1237
+ const userAddress = await this.walletService.getUserAddressWithDefault(accountId);
1238
+ // userFills returns a Promise<ISubscription>, need to await it
1239
+ const subscription = await subscriptionClient.userFills({ user: userAddress }, (data) => {
1240
+ const orderFills = data.fills.map((fill) => ({
1241
+ orderId: fill.oid.toString(),
1242
+ symbol: fill.coin,
1243
+ side: fill.side,
1244
+ size: fill.sz,
1245
+ price: fill.px,
1246
+ fee: fill.fee,
1247
+ timestamp: fill.time,
1248
+ pnl: fill.closedPnl,
1249
+ direction: fill.dir,
1250
+ feeToken: fill.feeToken,
1251
+ startPosition: fill.startPosition,
1252
+ liquidation: fill.liquidation
1253
+ ? {
1254
+ liquidatedUser: fill.liquidation.liquidatedUser,
1255
+ markPx: fill.liquidation.markPx,
1256
+ method: fill.liquidation.method,
1257
+ }
1258
+ : undefined,
1259
+ }));
1260
+ // Distribute to all callbacks for this accountId
1261
+ const subscribers = this.orderFillSubscribers.get(normalizedAccountId);
1262
+ if (subscribers) {
1263
+ subscribers.forEach((fillCallback) => {
1264
+ fillCallback(orderFills, data.isSnapshot);
1265
+ });
1266
+ }
1267
+ });
1268
+ this.orderFillSubscriptions.set(normalizedAccountId, subscription);
1269
+ }
1270
+ /**
1271
+ * Subscribe to live order updates
1272
+ * Uses the shared webData2 subscription to avoid duplicate connections
1273
+ *
1274
+ * @param params - Subscription parameters
1275
+ * @returns Cleanup function
1276
+ */
1277
+ subscribeToOrders(params) {
1278
+ const { callback, accountId } = params;
1279
+ const unsubscribe = this.createSubscription(this.orderSubscribers, callback);
1280
+ // Increment order subscriber count
1281
+ this.orderSubscriberCount++;
1282
+ // Immediately provide cached data if available
1283
+ if (this.cachedOrders) {
1284
+ callback(this.cachedOrders);
1285
+ }
1286
+ // Ensure shared subscription is active
1287
+ this.ensureSharedWebData3Subscription(accountId).catch((subError) => {
1288
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('subscribeToOrders'));
1289
+ });
1290
+ return () => {
1291
+ unsubscribe();
1292
+ this.orderSubscriberCount--;
1293
+ this.cleanupSharedWebData3ISubscription();
1294
+ };
1295
+ }
1296
+ /**
1297
+ * Subscribe to live account updates
1298
+ * Uses the shared webData2 subscription to avoid duplicate connections
1299
+ *
1300
+ * @param params - Subscription parameters
1301
+ * @returns Cleanup function
1302
+ */
1303
+ subscribeToAccount(params) {
1304
+ const { callback, accountId } = params;
1305
+ const unsubscribe = this.createSubscription(this.accountSubscribers, callback);
1306
+ // Increment account subscriber count
1307
+ this.accountSubscriberCount++;
1308
+ // Immediately provide cached data if available
1309
+ if (this.cachedAccount) {
1310
+ callback(this.cachedAccount);
1311
+ }
1312
+ // Ensure shared subscription is active (reuses existing connection)
1313
+ this.ensureSharedWebData3Subscription(accountId).catch((subError) => {
1314
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('subscribeToAccount'));
1315
+ });
1316
+ return () => {
1317
+ unsubscribe();
1318
+ this.accountSubscriberCount--;
1319
+ this.cleanupSharedWebData3ISubscription();
1320
+ };
1321
+ }
1322
+ /**
1323
+ * Check if orders cache has been initialized from WebSocket
1324
+ *
1325
+ * @returns true if WebSocket has sent at least one update, false otherwise
1326
+ */
1327
+ isOrdersCacheInitialized() {
1328
+ return this.ordersCacheInitialized;
1329
+ }
1330
+ /**
1331
+ * Check if positions cache has been initialized from WebSocket
1332
+ *
1333
+ * @returns true if WebSocket has sent at least one update, false otherwise
1334
+ */
1335
+ isPositionsCacheInitialized() {
1336
+ return this.positionsCacheInitialized;
1337
+ }
1338
+ /**
1339
+ * Get cached positions from WebSocket subscription
1340
+ *
1341
+ * @returns Cached positions array, or null if not initialized
1342
+ */
1343
+ getCachedPositions() {
1344
+ return this.cachedPositions;
1345
+ }
1346
+ /**
1347
+ * Get cached orders from WebSocket subscription
1348
+ *
1349
+ * @returns Cached orders array, or null if not initialized
1350
+ */
1351
+ getCachedOrders() {
1352
+ return this.cachedOrders;
1353
+ }
1354
+ /**
1355
+ * Create subscription with common error handling
1356
+ *
1357
+ * @param subscribers - Subscriber collection
1358
+ * @param callback - Callback to add
1359
+ * @param key - Optional key for Map-based collections
1360
+ * @returns Unsubscribe function
1361
+ */
1362
+ createSubscription(subscribers, callback, key) {
1363
+ if (subscribers instanceof Map && key) {
1364
+ if (!subscribers.has(key)) {
1365
+ subscribers.set(key, new Set());
1366
+ }
1367
+ subscribers.get(key)?.add(callback);
1368
+ }
1369
+ else if (subscribers instanceof Set) {
1370
+ subscribers.add(callback);
1371
+ }
1372
+ return () => {
1373
+ if (subscribers instanceof Map && key) {
1374
+ const set = subscribers.get(key);
1375
+ set?.delete(callback);
1376
+ if (set?.size === 0) {
1377
+ subscribers.delete(key);
1378
+ }
1379
+ }
1380
+ else if (subscribers instanceof Set) {
1381
+ subscribers.delete(callback);
1382
+ }
1383
+ };
1384
+ }
1385
+ /**
1386
+ * Helper function to create consolidated price updates with 24h change calculation
1387
+ *
1388
+ * @param symbol - Asset symbol
1389
+ * @param price - Current price
1390
+ * @returns Price update object
1391
+ */
1392
+ createPriceUpdate(symbol, price) {
1393
+ const marketData = this.marketDataCache.get(symbol);
1394
+ const orderBookData = this.orderBookCache.get(symbol);
1395
+ const currentPrice = parseFloat(price);
1396
+ let percentChange24h;
1397
+ if (marketData?.prevDayPx !== undefined) {
1398
+ const change = ((currentPrice - marketData.prevDayPx) / marketData.prevDayPx) * 100;
1399
+ percentChange24h = change.toFixed(2);
1400
+ }
1401
+ // Check if any subscriber for this symbol wants market data
1402
+ const hasMarketDataSubscribers = this.marketDataSubscribers.has(symbol) &&
1403
+ (this.marketDataSubscribers.get(symbol)?.size ?? 0) > 0;
1404
+ const priceUpdate = {
1405
+ symbol,
1406
+ price, // This is the mid price from allMids
1407
+ timestamp: Date.now(),
1408
+ percentChange24h,
1409
+ // Add mark price from activeAssetCtx
1410
+ markPrice: marketData?.oraclePrice
1411
+ ? marketData.oraclePrice.toString()
1412
+ : undefined,
1413
+ // Add order book data if available
1414
+ bestBid: orderBookData?.bestBid,
1415
+ bestAsk: orderBookData?.bestAsk,
1416
+ spread: orderBookData?.spread,
1417
+ // Always include funding when available (don't default to 0, preserve undefined)
1418
+ funding: marketData?.funding,
1419
+ // Add market data only if requested by at least one subscriber
1420
+ openInterest: hasMarketDataSubscribers
1421
+ ? marketData?.openInterest
1422
+ : undefined,
1423
+ volume24h: hasMarketDataSubscribers ? marketData?.volume24h : undefined,
1424
+ };
1425
+ return priceUpdate;
1426
+ }
1427
+ /**
1428
+ * Ensure global allMids subscription is active (singleton pattern)
1429
+ */
1430
+ ensureGlobalAllMidsSubscription() {
1431
+ // Check both the subscription AND the promise to prevent race conditions
1432
+ if (this.globalAllMidsSubscription || this.globalAllMidsPromise) {
1433
+ return;
1434
+ }
1435
+ const subscriptionClient = this.clientService.getSubscriptionClient();
1436
+ if (!subscriptionClient) {
1437
+ return;
1438
+ }
1439
+ // Track WebSocket metrics
1440
+ const wsMetrics = {
1441
+ messagesReceived: 0,
1442
+ lastMessageTime: Date.now(),
1443
+ reconnectCount: 0,
1444
+ startTime: Date.now(),
1445
+ };
1446
+ // Store the promise immediately to prevent duplicate calls
1447
+ this.globalAllMidsPromise = subscriptionClient
1448
+ .allMids((data) => {
1449
+ wsMetrics.messagesReceived++;
1450
+ wsMetrics.lastMessageTime = Date.now();
1451
+ // Initialize cache if needed
1452
+ this.cachedPriceData ?? (this.cachedPriceData = new Map());
1453
+ const subscribedSymbols = new Set();
1454
+ // Collect all symbols that have subscribers
1455
+ for (const [symbol, subscriberSet] of this.priceSubscribers.entries()) {
1456
+ if (subscriberSet.size > 0) {
1457
+ subscribedSymbols.add(symbol);
1458
+ }
1459
+ }
1460
+ // Track if any subscribed symbol was updated
1461
+ let hasUpdates = false;
1462
+ // Only process symbols that are actually subscribed to
1463
+ for (const symbol in data.mids) {
1464
+ // Skip if nobody is subscribed to this symbol
1465
+ if (!subscribedSymbols.has(symbol)) {
1466
+ continue;
1467
+ }
1468
+ const priceValue = data.mids[symbol].toString();
1469
+ const cachedPrice = this.cachedPriceData.get(symbol);
1470
+ // Skip if price hasn't changed
1471
+ if (cachedPrice?.price === priceValue) {
1472
+ continue;
1473
+ }
1474
+ // Price changed or new symbol - update cache
1475
+ const priceUpdate = this.createPriceUpdate(symbol, priceValue);
1476
+ this.cachedPriceData.set(symbol, priceUpdate);
1477
+ hasUpdates = true;
1478
+ }
1479
+ // Only notify subscribers if we actually have updates
1480
+ // This prevents unnecessary React re-renders when prices haven't changed
1481
+ if (hasUpdates) {
1482
+ this.notifyAllPriceSubscribers();
1483
+ }
1484
+ })
1485
+ .then((sub) => {
1486
+ this.globalAllMidsSubscription = sub;
1487
+ this.deps.debugLogger.log('HyperLiquid: Global allMids subscription established');
1488
+ // Notify existing subscribers with any cached data now that subscription is established
1489
+ if (this.cachedPriceData && this.cachedPriceData.size > 0) {
1490
+ this.notifyAllPriceSubscribers();
1491
+ }
1492
+ })
1493
+ .catch((subError) => {
1494
+ // Clear the promise on error so it can be retried
1495
+ this.globalAllMidsPromise = undefined;
1496
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('ensureGlobalAllMidsSubscription'));
1497
+ });
1498
+ }
1499
+ /**
1500
+ * Ensure activeAssetCtx subscription for specific symbol (with reference counting)
1501
+ *
1502
+ * @param symbol - Asset symbol
1503
+ */
1504
+ ensureActiveAssetSubscription(symbol) {
1505
+ // Increment subscriber count
1506
+ const currentCount = this.symbolSubscriberCounts.get(symbol) || 0;
1507
+ this.symbolSubscriberCounts.set(symbol, currentCount + 1);
1508
+ // If subscription already exists, just return
1509
+ if (this.globalActiveAssetSubscriptions.has(symbol)) {
1510
+ return;
1511
+ }
1512
+ const subscriptionClient = this.clientService.getSubscriptionClient();
1513
+ if (!subscriptionClient) {
1514
+ return;
1515
+ }
1516
+ // Track metrics for this subscription
1517
+ const subscriptionMetrics = {
1518
+ messagesReceived: 0,
1519
+ startTime: Date.now(),
1520
+ };
1521
+ subscriptionClient
1522
+ .activeAssetCtx({ coin: symbol }, (data) => {
1523
+ subscriptionMetrics.messagesReceived++;
1524
+ if (data.coin === symbol && data.ctx) {
1525
+ // Type guard using SDK types: check if this is perps (has funding) or spot (no funding)
1526
+ const isPerpsContext = (event) => 'funding' in event.ctx &&
1527
+ 'openInterest' in event.ctx &&
1528
+ 'oraclePx' in event.ctx;
1529
+ const { ctx } = data;
1530
+ // Cache market data for consolidation with price updates
1531
+ const ctxPrice = ctx.midPx || ctx.markPx;
1532
+ const openInterestUSD = isPerpsContext(data) && ctxPrice
1533
+ ? (0, marketDataTransform_1.calculateOpenInterestUSD)(data.ctx.openInterest, ctxPrice)
1534
+ : NaN;
1535
+ const marketDataEntry = {
1536
+ prevDayPx: ctx.prevDayPx
1537
+ ? parseFloat(ctx.prevDayPx.toString())
1538
+ : undefined,
1539
+ // Cache funding rate from activeAssetCtx for real-time updates
1540
+ // SDK defines funding as string (not nullable) in ActiveAssetCtxEvent
1541
+ funding: isPerpsContext(data)
1542
+ ? parseFloat(data.ctx.funding.toString())
1543
+ : undefined,
1544
+ openInterest: !isNaN(openInterestUSD)
1545
+ ? openInterestUSD
1546
+ : undefined,
1547
+ volume24h: ctx.dayNtlVlm
1548
+ ? parseFloat(ctx.dayNtlVlm.toString())
1549
+ : undefined,
1550
+ oraclePrice: isPerpsContext(data)
1551
+ ? parseFloat(data.ctx.oraclePx.toString())
1552
+ : undefined,
1553
+ lastUpdated: Date.now(),
1554
+ };
1555
+ this.marketDataCache.set(symbol, marketDataEntry);
1556
+ // Update cached price data with new 24h change if we have current price
1557
+ const currentCachedPrice = this.cachedPriceData?.get(symbol);
1558
+ if (currentCachedPrice) {
1559
+ const updatedPrice = this.createPriceUpdate(symbol, currentCachedPrice.price);
1560
+ this.cachedPriceData ?? (this.cachedPriceData = new Map());
1561
+ this.cachedPriceData.set(symbol, updatedPrice);
1562
+ this.notifyAllPriceSubscribers();
1563
+ }
1564
+ }
1565
+ })
1566
+ .then((sub) => {
1567
+ this.globalActiveAssetSubscriptions.set(symbol, sub);
1568
+ this.deps.debugLogger.log(`HyperLiquid: Market data subscription established for ${symbol}`);
1569
+ })
1570
+ .catch((subError) => {
1571
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('ensureActiveAssetSubscription', { symbol }));
1572
+ });
1573
+ }
1574
+ /**
1575
+ * Cleanup activeAssetCtx subscription when no longer needed
1576
+ *
1577
+ * @param symbol - Asset symbol
1578
+ */
1579
+ cleanupActiveAssetSubscription(symbol) {
1580
+ const currentCount = this.symbolSubscriberCounts.get(symbol) || 0;
1581
+ if (currentCount <= 1) {
1582
+ // Last subscriber, cleanup subscription
1583
+ const subscription = this.globalActiveAssetSubscriptions.get(symbol);
1584
+ if (subscription && typeof subscription.unsubscribe === 'function') {
1585
+ const unsubscribeResult = Promise.resolve(subscription.unsubscribe());
1586
+ unsubscribeResult.catch(() => {
1587
+ // Ignore errors during cleanup
1588
+ });
1589
+ this.globalActiveAssetSubscriptions.delete(symbol);
1590
+ this.symbolSubscriberCounts.delete(symbol);
1591
+ }
1592
+ else if (subscription) {
1593
+ // Subscription exists but unsubscribe is not a function or doesn't return a Promise
1594
+ // Just clean up the reference
1595
+ this.globalActiveAssetSubscriptions.delete(symbol);
1596
+ this.symbolSubscriberCounts.delete(symbol);
1597
+ }
1598
+ }
1599
+ else {
1600
+ // Still has subscribers, just decrement count
1601
+ this.symbolSubscriberCounts.set(symbol, currentCount - 1);
1602
+ }
1603
+ }
1604
+ /**
1605
+ * Ensure assetCtxs subscription for specific DEX (HIP-3 support)
1606
+ * Uses WebSocket instead of REST polling for market data
1607
+ * Implements reference counting to track active subscribers per DEX
1608
+ *
1609
+ * @param dex - DEX identifier ('' for main)
1610
+ */
1611
+ async ensureAssetCtxsSubscription(dex) {
1612
+ const dexKey = dex || '';
1613
+ // Increment subscriber count for this DEX
1614
+ const currentCount = this.dexSubscriberCounts.get(dexKey) || 0;
1615
+ this.dexSubscriberCounts.set(dexKey, currentCount + 1);
1616
+ // Return if subscription already exists
1617
+ if (this.assetCtxsSubscriptions.has(dexKey)) {
1618
+ return;
1619
+ }
1620
+ // Return existing promise if subscription is being established
1621
+ if (this.assetCtxsSubscriptionPromises.has(dexKey)) {
1622
+ return this.assetCtxsSubscriptionPromises.get(dexKey);
1623
+ }
1624
+ // Create new subscription promise
1625
+ const promise = this.createAssetCtxsSubscription(dex);
1626
+ this.assetCtxsSubscriptionPromises.set(dexKey, promise);
1627
+ try {
1628
+ await promise;
1629
+ }
1630
+ catch {
1631
+ // Clear promise on error so it can be retried
1632
+ this.assetCtxsSubscriptionPromises.delete(dexKey);
1633
+ throw new Error(`Failed to create assetCtxs subscription for DEX: ${dex}`);
1634
+ }
1635
+ }
1636
+ /**
1637
+ * Create assetCtxs subscription for specific DEX
1638
+ * Provides real-time market data for all assets on the DEX
1639
+ *
1640
+ * Performance: Uses cached meta from dexMetaCache (populated by metaAndAssetCtxs)
1641
+ * to avoid redundant meta() API calls during subscription setup
1642
+ *
1643
+ * @param dex - DEX identifier ('' for main)
1644
+ */
1645
+ async createAssetCtxsSubscription(dex) {
1646
+ await this.clientService.ensureSubscriptionClient(this.walletService.createWalletAdapter());
1647
+ const subscriptionClient = this.clientService.getSubscriptionClient();
1648
+ if (!subscriptionClient) {
1649
+ throw new Error('Subscription client not initialized');
1650
+ }
1651
+ const dexKey = dex || '';
1652
+ const dexIdentifier = dex ?? 'main DEX';
1653
+ // Check cache first - populated by metaAndAssetCtxs in ensureAssetCtxsSubscription
1654
+ let perpsMeta = this.dexMetaCache.get(dexKey);
1655
+ if (!perpsMeta) {
1656
+ // Fallback: fetch meta if not in cache (shouldn't happen in normal flow)
1657
+ this.deps.debugLogger.log(`Meta cache miss for ${dexIdentifier}, fetching from API`);
1658
+ const infoClient = this.clientService.getInfoClient();
1659
+ const fetchedMeta = await infoClient.meta({ dex: dex || undefined });
1660
+ if (fetchedMeta?.universe) {
1661
+ perpsMeta = fetchedMeta;
1662
+ this.dexMetaCache.set(dexKey, fetchedMeta);
1663
+ }
1664
+ }
1665
+ if (!perpsMeta?.universe) {
1666
+ const errorMessage = `No universe data available for ${dexIdentifier}`;
1667
+ throw new Error(errorMessage);
1668
+ }
1669
+ this.deps.debugLogger.log(`Using ${this.dexMetaCache.has(dexKey) ? 'cached' : 'fetched'} meta for ${dexIdentifier}`, {
1670
+ dex,
1671
+ universeCount: perpsMeta.universe.length,
1672
+ firstAssetSample: perpsMeta.universe[0]?.name,
1673
+ });
1674
+ // Capture perpsMeta in closure for callback
1675
+ const metaForCallback = perpsMeta;
1676
+ return new Promise((resolve, reject) => {
1677
+ const subscriptionParams = dex ? { dex } : {};
1678
+ subscriptionClient
1679
+ .assetCtxs(subscriptionParams, (data) => {
1680
+ // Cache asset contexts for this DEX
1681
+ this.dexAssetCtxsCache.set(dexKey, data.ctxs);
1682
+ // Use cached meta to map ctxs array indices to symbols (no REST API call!)
1683
+ metaForCallback.universe.forEach((asset, index) => {
1684
+ const ctx = data.ctxs[index];
1685
+ if (ctx && 'funding' in ctx) {
1686
+ // This is a perps context
1687
+ const ctxPrice = ctx.midPx || ctx.markPx;
1688
+ const openInterestUSD = (0, marketDataTransform_1.calculateOpenInterestUSD)(ctx.openInterest, ctxPrice);
1689
+ const marketDataEntry = {
1690
+ prevDayPx: ctx.prevDayPx
1691
+ ? parseFloat(ctx.prevDayPx.toString())
1692
+ : undefined,
1693
+ funding: parseFloat(ctx.funding.toString()),
1694
+ openInterest: !isNaN(openInterestUSD)
1695
+ ? openInterestUSD
1696
+ : undefined,
1697
+ volume24h: ctx.dayNtlVlm
1698
+ ? parseFloat(ctx.dayNtlVlm.toString())
1699
+ : undefined,
1700
+ oraclePrice: parseFloat(ctx.oraclePx.toString()),
1701
+ lastUpdated: Date.now(),
1702
+ };
1703
+ this.marketDataCache.set(asset.name, marketDataEntry);
1704
+ // HIP-3: Extract price from assetCtx and update cached prices
1705
+ const priceValue = ctx.midPx?.toString() || ctx.markPx?.toString();
1706
+ if (priceValue) {
1707
+ // For HIP-3 DEXs, meta() returns asset.name already containing the DEX prefix
1708
+ // (e.g., "xyz:XYZ100"), so use it directly
1709
+ const assetSymbol = asset.name;
1710
+ const priceUpdate = this.createPriceUpdate(assetSymbol, priceValue);
1711
+ this.cachedPriceData ?? (this.cachedPriceData = new Map());
1712
+ this.cachedPriceData.set(assetSymbol, priceUpdate);
1713
+ }
1714
+ }
1715
+ });
1716
+ // Notify price subscribers with updated market data
1717
+ this.notifyAllPriceSubscribers();
1718
+ })
1719
+ .then((sub) => {
1720
+ this.assetCtxsSubscriptions.set(dexKey, sub);
1721
+ this.deps.debugLogger.log(`assetCtxs subscription established for ${dex ? `DEX: ${dex}` : 'main DEX'}`);
1722
+ resolve();
1723
+ })
1724
+ .catch((subError) => {
1725
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('createAssetCtxsSubscription', { dex }));
1726
+ reject(ensureError(subError));
1727
+ });
1728
+ });
1729
+ }
1730
+ /**
1731
+ * Cleanup assetCtxs subscription for specific DEX with reference counting
1732
+ * Only unsubscribes when the last subscriber for this DEX is removed
1733
+ *
1734
+ * @param dex - DEX identifier ('' for main)
1735
+ */
1736
+ cleanupAssetCtxsSubscription(dex) {
1737
+ const dexKey = dex || '';
1738
+ // Decrement subscriber count for this DEX
1739
+ const currentCount = this.dexSubscriberCounts.get(dexKey) || 0;
1740
+ if (currentCount <= 1) {
1741
+ // Last subscriber - cleanup the subscription
1742
+ const subscription = this.assetCtxsSubscriptions.get(dexKey);
1743
+ if (subscription) {
1744
+ subscription.unsubscribe().catch((unsubError) => {
1745
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('cleanupAssetCtxsSubscription', { dex }));
1746
+ });
1747
+ this.assetCtxsSubscriptions.delete(dexKey);
1748
+ this.dexAssetCtxsCache.delete(dexKey);
1749
+ this.assetCtxsSubscriptionPromises.delete(dexKey);
1750
+ this.dexSubscriberCounts.delete(dexKey);
1751
+ this.deps.debugLogger.log(`Cleaned up assetCtxs subscription for ${dex ? `DEX: ${dex}` : 'main DEX'}`);
1752
+ }
1753
+ }
1754
+ else {
1755
+ // Still has subscribers - just decrement count
1756
+ this.dexSubscriberCounts.set(dexKey, currentCount - 1);
1757
+ }
1758
+ }
1759
+ /**
1760
+ * Ensure BBO subscription for specific symbol (singleton)
1761
+ *
1762
+ * BBO provides best bid/ask without being affected by L2Book aggregation parameters,
1763
+ * keeping spread consistent across order book grouping selections (matches Hyperliquid UI).
1764
+ *
1765
+ * @param symbol - Asset symbol
1766
+ */
1767
+ ensureBboSubscription(symbol) {
1768
+ if (this.globalBboSubscriptions.has(symbol)) {
1769
+ return;
1770
+ }
1771
+ const subscriptionClient = this.clientService.getSubscriptionClient();
1772
+ if (!subscriptionClient) {
1773
+ return;
1774
+ }
1775
+ subscriptionClient
1776
+ .bbo({ coin: symbol }, (data) => {
1777
+ (0, hyperLiquidOrderBookProcessor_1.processBboData)({
1778
+ symbol,
1779
+ data,
1780
+ orderBookCache: this.orderBookCache,
1781
+ cachedPriceData: this.cachedPriceData,
1782
+ createPriceUpdate: this.createPriceUpdate.bind(this),
1783
+ notifySubscribers: this.notifyAllPriceSubscribers.bind(this),
1784
+ });
1785
+ })
1786
+ .then((sub) => {
1787
+ this.globalBboSubscriptions.set(symbol, sub);
1788
+ this.deps.debugLogger.log(`HyperLiquid: BBO subscription established for ${symbol}`);
1789
+ })
1790
+ .catch((subError) => {
1791
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('ensureBboSubscription', { symbol }));
1792
+ });
1793
+ }
1794
+ /**
1795
+ * Cleanup BBO subscription when no longer needed
1796
+ *
1797
+ * @param symbol - Asset symbol
1798
+ */
1799
+ cleanupBboSubscription(symbol) {
1800
+ // If anyone still wants order book (top-of-book) data for this symbol, keep the subscription alive.
1801
+ if ((this.orderBookSubscribers.get(symbol)?.size ?? 0) > 0) {
1802
+ return;
1803
+ }
1804
+ const subscription = this.globalBboSubscriptions.get(symbol);
1805
+ if (subscription && typeof subscription.unsubscribe === 'function') {
1806
+ const unsubscribeResult = Promise.resolve(subscription.unsubscribe());
1807
+ unsubscribeResult.catch(() => {
1808
+ // Ignore errors during cleanup
1809
+ });
1810
+ this.globalBboSubscriptions.delete(symbol);
1811
+ this.orderBookCache.delete(symbol);
1812
+ }
1813
+ else if (subscription) {
1814
+ // Subscription exists but unsubscribe is not a function or doesn't return a Promise
1815
+ // Just clean up the reference
1816
+ this.globalBboSubscriptions.delete(symbol);
1817
+ this.orderBookCache.delete(symbol);
1818
+ }
1819
+ }
1820
+ /**
1821
+ * Subscribe to full order book updates with multiple depth levels
1822
+ * Creates a dedicated L2Book subscription for the requested symbol
1823
+ * and processes data into OrderBookData format for UI consumption
1824
+ *
1825
+ * @param params - Subscription parameters
1826
+ * @returns Cleanup function to unsubscribe
1827
+ */
1828
+ subscribeToOrderBook(params) {
1829
+ const { symbol, levels = 10, nSigFigs = 5, mantissa, callback, onError, } = params;
1830
+ this.clientService.ensureSubscriptionClient(this.walletService.createWalletAdapter());
1831
+ const subscriptionClient = this.clientService.getSubscriptionClient();
1832
+ if (!subscriptionClient) {
1833
+ const subError = new Error('Subscription client not available');
1834
+ onError?.(subError);
1835
+ this.deps.debugLogger.log('subscribeToOrderBook: Subscription client not available');
1836
+ return () => {
1837
+ // No-op cleanup
1838
+ };
1839
+ }
1840
+ let subscription;
1841
+ let cancelled = false;
1842
+ subscriptionClient
1843
+ .l2Book({ coin: symbol, nSigFigs, mantissa }, (data) => {
1844
+ if (cancelled || data?.coin !== symbol || !data?.levels) {
1845
+ return;
1846
+ }
1847
+ const orderBookData = this.processOrderBookData(data, levels);
1848
+ callback(orderBookData);
1849
+ })
1850
+ .then((sub) => {
1851
+ if (cancelled) {
1852
+ sub.unsubscribe().catch((unsubError) => {
1853
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('subscribeToOrderBook.cleanup', { symbol }));
1854
+ });
1855
+ }
1856
+ else {
1857
+ subscription = sub;
1858
+ this.deps.debugLogger.log(`HyperLiquid: Order book subscription established for ${symbol}`);
1859
+ }
1860
+ })
1861
+ .catch((subError) => {
1862
+ this.deps.logger.error(ensureError(subError), this.getErrorContext('subscribeToOrderBook', { symbol }));
1863
+ onError?.(ensureError(subError));
1864
+ });
1865
+ return () => {
1866
+ cancelled = true;
1867
+ if (subscription) {
1868
+ subscription.unsubscribe().catch((unsubError) => {
1869
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('subscribeToOrderBook.unsubscribe', {
1870
+ symbol,
1871
+ }));
1872
+ });
1873
+ }
1874
+ };
1875
+ }
1876
+ /**
1877
+ * Process raw L2Book data into OrderBookData format
1878
+ * Calculates cumulative totals, notional values, and spread metrics
1879
+ *
1880
+ * @param data - Raw L2Book response from WebSocket
1881
+ * @param levels - Number of levels to return per side
1882
+ * @returns Processed OrderBookData
1883
+ */
1884
+ processOrderBookData(data, levels) {
1885
+ const bidsRaw = data?.levels?.[0] || [];
1886
+ const asksRaw = data?.levels?.[1] || [];
1887
+ // Process bids (buy orders) - highest price first
1888
+ let bidCumulativeSize = 0;
1889
+ let bidCumulativeNotional = 0;
1890
+ const bids = bidsRaw.slice(0, levels).map((level) => {
1891
+ const price = parseFloat(level.px);
1892
+ const size = parseFloat(level.sz);
1893
+ const notional = price * size;
1894
+ bidCumulativeSize += size;
1895
+ bidCumulativeNotional += notional;
1896
+ return {
1897
+ price: level.px,
1898
+ size: level.sz,
1899
+ total: bidCumulativeSize.toString(),
1900
+ notional: notional.toFixed(2),
1901
+ totalNotional: bidCumulativeNotional.toFixed(2),
1902
+ };
1903
+ });
1904
+ // Process asks (sell orders) - lowest price first
1905
+ let askCumulativeSize = 0;
1906
+ let askCumulativeNotional = 0;
1907
+ const asks = asksRaw.slice(0, levels).map((level) => {
1908
+ const price = parseFloat(level.px);
1909
+ const size = parseFloat(level.sz);
1910
+ const notional = price * size;
1911
+ askCumulativeSize += size;
1912
+ askCumulativeNotional += notional;
1913
+ return {
1914
+ price: level.px,
1915
+ size: level.sz,
1916
+ total: askCumulativeSize.toString(),
1917
+ notional: notional.toFixed(2),
1918
+ totalNotional: askCumulativeNotional.toFixed(2),
1919
+ };
1920
+ });
1921
+ // Calculate spread and mid price
1922
+ const bestBid = bids[0];
1923
+ const bestAsk = asks[0];
1924
+ const bidPrice = bestBid ? parseFloat(bestBid.price) : 0;
1925
+ const askPrice = bestAsk ? parseFloat(bestAsk.price) : 0;
1926
+ const spread = askPrice > 0 && bidPrice > 0 ? askPrice - bidPrice : 0;
1927
+ const midPrice = askPrice > 0 && bidPrice > 0 ? (askPrice + bidPrice) / 2 : 0;
1928
+ const spreadPercentage = midPrice > 0 ? ((spread / midPrice) * 100).toFixed(4) : '0';
1929
+ // Calculate max total for depth chart scaling
1930
+ const maxTotal = Math.max(bidCumulativeSize, askCumulativeSize).toString();
1931
+ return {
1932
+ bids,
1933
+ asks,
1934
+ spread: spread.toFixed(5),
1935
+ spreadPercentage,
1936
+ midPrice: midPrice.toFixed(5),
1937
+ lastUpdated: Date.now(),
1938
+ maxTotal,
1939
+ };
1940
+ }
1941
+ /**
1942
+ * Notify all price subscribers with their requested symbols from cache
1943
+ * Optimized to batch updates per subscriber
1944
+ */
1945
+ notifyAllPriceSubscribers() {
1946
+ // If no price data exists yet, don't notify
1947
+ if (!this.cachedPriceData) {
1948
+ return;
1949
+ }
1950
+ const priceData = this.cachedPriceData;
1951
+ // Group updates by subscriber to batch notifications
1952
+ const subscriberUpdates = new Map();
1953
+ this.priceSubscribers.forEach((subscriberSet, symbol) => {
1954
+ const priceUpdate = priceData.get(symbol);
1955
+ if (priceUpdate) {
1956
+ subscriberSet.forEach((priceCallback) => {
1957
+ if (!subscriberUpdates.has(priceCallback)) {
1958
+ subscriberUpdates.set(priceCallback, []);
1959
+ }
1960
+ const updates = subscriberUpdates.get(priceCallback);
1961
+ if (updates) {
1962
+ updates.push(priceUpdate);
1963
+ }
1964
+ });
1965
+ }
1966
+ });
1967
+ // Send batched updates to each subscriber
1968
+ subscriberUpdates.forEach((updates, priceCallback) => {
1969
+ if (updates.length > 0) {
1970
+ priceCallback(updates);
1971
+ }
1972
+ });
1973
+ }
1974
+ /**
1975
+ * Restore all active subscriptions after WebSocket reconnection
1976
+ * Re-establishes WebSocket subscriptions for all active subscribers
1977
+ *
1978
+ * IMPORTANT: This method verifies transport readiness before attempting
1979
+ * any subscriptions to prevent "subscribe error: undefined" errors.
1980
+ */
1981
+ async restoreSubscriptions() {
1982
+ // CRITICAL: Verify transport is ready before attempting any subscriptions
1983
+ // This prevents race conditions where subscriptions are attempted while
1984
+ // the WebSocket is still in CONNECTING state
1985
+ try {
1986
+ await this.clientService.ensureTransportReady(5000);
1987
+ }
1988
+ catch (transportError) {
1989
+ this.deps.debugLogger.log('Transport not ready during subscription restore, will retry on next reconnect', {
1990
+ error: transportError instanceof Error
1991
+ ? transportError.message
1992
+ : String(transportError),
1993
+ });
1994
+ return;
1995
+ }
1996
+ // Re-establish global allMids subscription if there are price subscribers
1997
+ if (this.priceSubscribers.size > 0) {
1998
+ // Clear existing subscription reference (it's dead after reconnection)
1999
+ this.globalAllMidsSubscription = undefined;
2000
+ this.globalAllMidsPromise = undefined;
2001
+ // Re-establish the subscription
2002
+ this.ensureGlobalAllMidsSubscription();
2003
+ }
2004
+ // Re-establish order fill subscriptions if there are fill subscribers
2005
+ if (this.orderFillSubscribers.size > 0) {
2006
+ // Clear existing subscription references (they're dead after reconnection)
2007
+ this.orderFillSubscriptions.clear();
2008
+ // Re-establish subscriptions for all accountIds with subscribers
2009
+ // Note: normalizedAccountId is 'default' for undefined, need to convert back
2010
+ const normalizedAccountIds = Array.from(this.orderFillSubscribers.keys());
2011
+ await Promise.all(normalizedAccountIds.map((normalizedAccountId) => {
2012
+ // Convert normalized key back to original accountId (undefined if 'default')
2013
+ const originalAccountId = normalizedAccountId === 'default'
2014
+ ? undefined
2015
+ : normalizedAccountId;
2016
+ return this.ensureOrderFillISubscription(originalAccountId).catch(() => {
2017
+ // Ignore errors during order fill subscription restoration
2018
+ });
2019
+ }));
2020
+ }
2021
+ // Re-establish user data subscriptions if there are user data subscribers
2022
+ if (this.positionSubscribers.size > 0 ||
2023
+ this.orderSubscribers.size > 0 ||
2024
+ this.accountSubscribers.size > 0 ||
2025
+ this.oiCapSubscribers.size > 0) {
2026
+ // Clear existing subscription references (they're dead after reconnection)
2027
+ this.webData3Subscriptions.clear();
2028
+ this.webData3SubscriptionPromise = undefined;
2029
+ // Clear individual subscriptions (clearinghouseState + openOrders) for HIP-3 mode
2030
+ this.clearinghouseStateSubscriptions.clear();
2031
+ this.openOrdersSubscriptions.clear();
2032
+ // Re-establish the subscription (will use current account)
2033
+ // This will set up webData2 for non-HIP-3, or individual subscriptions + webData3 (OI caps only) for HIP-3
2034
+ await this.ensureSharedWebData3Subscription();
2035
+ }
2036
+ // Re-establish activeAsset subscriptions if there are market data subscribers
2037
+ if (this.marketDataSubscribers.size > 0) {
2038
+ // Clear existing subscriptions (they're dead after reconnection)
2039
+ this.globalActiveAssetSubscriptions.clear();
2040
+ // Clear reference counts to prevent double-counting after reconnection
2041
+ this.symbolSubscriberCounts.clear();
2042
+ // Re-establish subscriptions for all symbols with market data subscribers
2043
+ const symbolsNeedingMarketData = Array.from(this.marketDataSubscribers.keys());
2044
+ symbolsNeedingMarketData.forEach((symbol) => {
2045
+ this.ensureActiveAssetSubscription(symbol);
2046
+ });
2047
+ }
2048
+ // Re-establish BBO subscriptions if there are order book subscribers
2049
+ if (this.orderBookSubscribers.size > 0) {
2050
+ // Clear existing subscriptions (they're dead after reconnection)
2051
+ this.globalBboSubscriptions.clear();
2052
+ // Re-establish subscriptions for all symbols with order book subscribers
2053
+ const symbolsNeedingOrderBook = Array.from(this.orderBookSubscribers.keys());
2054
+ symbolsNeedingOrderBook.forEach((symbol) => {
2055
+ this.ensureBboSubscription(symbol);
2056
+ });
2057
+ }
2058
+ // Re-establish assetCtxs subscriptions if there are market data subscribers
2059
+ if (this.marketDataSubscribers.size > 0) {
2060
+ // Clear existing subscriptions (they're dead after reconnection)
2061
+ this.assetCtxsSubscriptions.clear();
2062
+ this.assetCtxsSubscriptionPromises.clear();
2063
+ // Clear reference counts to prevent double-counting after reconnection
2064
+ this.dexSubscriberCounts.clear();
2065
+ // Re-establish subscriptions for all DEXs with market data subscribers
2066
+ const dexsNeeded = new Set();
2067
+ this.marketDataSubscribers.forEach((_subscribers, symbol) => {
2068
+ const { dex } = (0, hyperLiquidAdapter_1.parseAssetName)(symbol);
2069
+ if (dex) {
2070
+ dexsNeeded.add(dex);
2071
+ }
2072
+ });
2073
+ // Add main DEX if any main DEX symbols have subscribers
2074
+ const hasMainDexSubscribers = Array.from(this.marketDataSubscribers.keys()).some((symbol) => {
2075
+ const { dex } = (0, hyperLiquidAdapter_1.parseAssetName)(symbol);
2076
+ return !dex;
2077
+ });
2078
+ if (hasMainDexSubscribers) {
2079
+ dexsNeeded.add('');
2080
+ }
2081
+ // Re-establish subscriptions
2082
+ await Promise.all(Array.from(dexsNeeded).map((dex) => this.ensureAssetCtxsSubscription(dex).catch(() => {
2083
+ // Ignore errors during assetCtxs subscription restoration
2084
+ })));
2085
+ }
2086
+ }
2087
+ /**
2088
+ * Clear all subscriptions and cached data (multi-DEX support)
2089
+ */
2090
+ clearAll() {
2091
+ // Clear all local subscriber collections
2092
+ this.priceSubscribers.clear();
2093
+ this.positionSubscribers.clear();
2094
+ this.orderFillSubscribers.clear();
2095
+ this.orderSubscribers.clear();
2096
+ this.accountSubscribers.clear();
2097
+ this.marketDataSubscribers.clear();
2098
+ this.orderBookSubscribers.clear();
2099
+ // Clear order fill subscriptions
2100
+ this.orderFillSubscriptions.forEach((subscription) => {
2101
+ subscription.unsubscribe().catch(() => {
2102
+ // Ignore errors during cleanup
2103
+ });
2104
+ });
2105
+ this.orderFillSubscriptions.clear();
2106
+ // Clear cached data
2107
+ this.cachedPriceData = null;
2108
+ this.cachedPositions = null;
2109
+ this.cachedOrders = null;
2110
+ this.cachedAccount = null;
2111
+ this.ordersCacheInitialized = false; // Reset cache initialization flag
2112
+ this.positionsCacheInitialized = false; // Reset cache initialization flag
2113
+ this.marketDataCache.clear();
2114
+ this.orderBookCache.clear();
2115
+ this.symbolSubscriberCounts.clear();
2116
+ this.dexSubscriberCounts.clear();
2117
+ // Clear hash caches
2118
+ this.cachedPositionsHash = '';
2119
+ this.cachedOrdersHash = '';
2120
+ this.cachedAccountHash = '';
2121
+ // Clear multi-DEX caches
2122
+ this.deps.debugLogger.log('HyperLiquidSubscriptionService: Clearing per-DEX caches', {
2123
+ dexPositionsCacheSize: this.dexPositionsCache.size,
2124
+ dexOrdersCacheSize: this.dexOrdersCache.size,
2125
+ dexAccountCacheSize: this.dexAccountCache.size,
2126
+ dexAssetCtxsCacheSize: this.dexAssetCtxsCache.size,
2127
+ dexPositionsCacheKeys: Array.from(this.dexPositionsCache.keys()),
2128
+ dexAssetCtxsCacheKeys: Array.from(this.dexAssetCtxsCache.keys()),
2129
+ });
2130
+ this.dexPositionsCache.clear();
2131
+ this.dexOrdersCache.clear();
2132
+ this.dexAccountCache.clear();
2133
+ this.dexAssetCtxsCache.clear();
2134
+ // Clear subscription references (actual cleanup handled by client service)
2135
+ this.globalAllMidsSubscription = undefined;
2136
+ this.globalActiveAssetSubscriptions.clear();
2137
+ this.globalBboSubscriptions.clear();
2138
+ this.webData3Subscriptions.clear();
2139
+ this.webData3SubscriptionPromise = undefined;
2140
+ // HIP-3: Clear assetCtxs subscriptions (clearinghouseState no longer needed with webData3)
2141
+ this.assetCtxsSubscriptions.clear();
2142
+ this.assetCtxsSubscriptionPromises.clear();
2143
+ // Cleanup individual subscriptions (clearinghouseState + openOrders)
2144
+ if (this.clearinghouseStateSubscriptions.size > 0) {
2145
+ this.clearinghouseStateSubscriptions.forEach((subscription, dexNameKey) => {
2146
+ subscription.unsubscribe().catch((unsubError) => {
2147
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('clearAll.clearinghouseState', {
2148
+ dex: dexNameKey,
2149
+ }));
2150
+ });
2151
+ });
2152
+ this.clearinghouseStateSubscriptions.clear();
2153
+ }
2154
+ if (this.openOrdersSubscriptions.size > 0) {
2155
+ this.openOrdersSubscriptions.forEach((subscription, dexNameKey) => {
2156
+ subscription.unsubscribe().catch((unsubError) => {
2157
+ this.deps.logger.error(ensureError(unsubError), this.getErrorContext('clearAll.openOrders', {
2158
+ dex: dexNameKey,
2159
+ }));
2160
+ });
2161
+ });
2162
+ this.openOrdersSubscriptions.clear();
2163
+ }
2164
+ this.deps.debugLogger.log('HyperLiquid: Subscription service cleared (multi-DEX with individual subscriptions)', {
2165
+ timestamp: new Date().toISOString(),
2166
+ });
2167
+ }
2168
+ }
2169
+ exports.HyperLiquidSubscriptionService = HyperLiquidSubscriptionService;
2170
+ //# sourceMappingURL=HyperLiquidSubscriptionService.cjs.map