@lifi/perps-sdk-provider-hyperliquid 15.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/cjs/HyperliquidProvider.d.ts.map +1 -1
  2. package/dist/cjs/HyperliquidProvider.js +5 -1
  3. package/dist/cjs/HyperliquidProvider.js.map +1 -1
  4. package/dist/cjs/accountSummary.d.ts.map +1 -1
  5. package/dist/cjs/accountSummary.js +34 -7
  6. package/dist/cjs/accountSummary.js.map +1 -1
  7. package/dist/cjs/index.d.ts +2 -0
  8. package/dist/cjs/index.d.ts.map +1 -1
  9. package/dist/cjs/index.js +3 -1
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/cjs/services/activeAssetData.d.ts +6 -0
  12. package/dist/cjs/services/activeAssetData.d.ts.map +1 -0
  13. package/dist/cjs/services/activeAssetData.js +7 -0
  14. package/dist/cjs/services/activeAssetData.js.map +1 -0
  15. package/dist/cjs/services/getAccount.d.ts.map +1 -1
  16. package/dist/cjs/services/getAccount.js +23 -35
  17. package/dist/cjs/services/getAccount.js.map +1 -1
  18. package/dist/cjs/services/getAvailableToTrade.d.ts +10 -0
  19. package/dist/cjs/services/getAvailableToTrade.d.ts.map +1 -0
  20. package/dist/cjs/services/getAvailableToTrade.js +32 -0
  21. package/dist/cjs/services/getAvailableToTrade.js.map +1 -0
  22. package/dist/cjs/services/getMarketSettings.d.ts +1 -1
  23. package/dist/cjs/services/getMarketSettings.d.ts.map +1 -1
  24. package/dist/cjs/services/getMarketSettings.js +3 -7
  25. package/dist/cjs/services/getMarketSettings.js.map +1 -1
  26. package/dist/cjs/services/getOrder.d.ts.map +1 -1
  27. package/dist/cjs/services/getOrder.js +7 -3
  28. package/dist/cjs/services/getOrder.js.map +1 -1
  29. package/dist/cjs/services/getOrders.d.ts.map +1 -1
  30. package/dist/cjs/services/getOrders.js +4 -23
  31. package/dist/cjs/services/getOrders.js.map +1 -1
  32. package/dist/cjs/services/getWithdrawableBalances.d.ts +5 -0
  33. package/dist/cjs/services/getWithdrawableBalances.d.ts.map +1 -0
  34. package/dist/cjs/services/getWithdrawableBalances.js +29 -0
  35. package/dist/cjs/services/getWithdrawableBalances.js.map +1 -0
  36. package/dist/cjs/types/account.d.ts +11 -8
  37. package/dist/cjs/types/account.d.ts.map +1 -1
  38. package/dist/cjs/types/account.js.map +1 -1
  39. package/dist/cjs/types/ws.d.ts +3 -4
  40. package/dist/cjs/types/ws.d.ts.map +1 -1
  41. package/dist/cjs/utils/abstractionMode.d.ts +1 -1
  42. package/dist/cjs/utils/abstractionMode.d.ts.map +1 -1
  43. package/dist/cjs/utils/abstractionMode.js.map +1 -1
  44. package/dist/cjs/utils/explorer.d.ts +2 -1
  45. package/dist/cjs/utils/explorer.d.ts.map +1 -1
  46. package/dist/cjs/utils/explorer.js +24 -1
  47. package/dist/cjs/utils/explorer.js.map +1 -1
  48. package/dist/cjs/utils/index.d.ts +3 -1
  49. package/dist/cjs/utils/index.d.ts.map +1 -1
  50. package/dist/cjs/utils/index.js +7 -2
  51. package/dist/cjs/utils/index.js.map +1 -1
  52. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  53. package/dist/cjs/utils/mapPosition.js +1 -4
  54. package/dist/cjs/utils/mapPosition.js.map +1 -1
  55. package/dist/cjs/utils/spotCollateral.d.ts +1 -1
  56. package/dist/cjs/utils/spotCollateral.d.ts.map +1 -1
  57. package/dist/cjs/utils/spotCollateral.js +1 -12
  58. package/dist/cjs/utils/spotCollateral.js.map +1 -1
  59. package/dist/cjs/utils/transferMargin.d.ts +2 -2
  60. package/dist/cjs/utils/transferMargin.d.ts.map +1 -1
  61. package/dist/cjs/utils/transferMargin.js +20 -12
  62. package/dist/cjs/utils/transferMargin.js.map +1 -1
  63. package/dist/cjs/utils/venueTotals.d.ts +11 -0
  64. package/dist/cjs/utils/venueTotals.d.ts.map +1 -0
  65. package/dist/cjs/utils/venueTotals.js +24 -0
  66. package/dist/cjs/utils/venueTotals.js.map +1 -0
  67. package/dist/cjs/utils/withdrawableBalances.d.ts +4 -0
  68. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -0
  69. package/dist/cjs/utils/withdrawableBalances.js +42 -0
  70. package/dist/cjs/utils/withdrawableBalances.js.map +1 -0
  71. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +3 -1
  72. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  73. package/dist/cjs/websocket/HyperliquidWsProvider.js +102 -36
  74. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  75. package/dist/esm/HyperliquidProvider.d.ts.map +1 -1
  76. package/dist/esm/HyperliquidProvider.js +6 -2
  77. package/dist/esm/HyperliquidProvider.js.map +1 -1
  78. package/dist/esm/accountSummary.d.ts +4 -3
  79. package/dist/esm/accountSummary.d.ts.map +1 -1
  80. package/dist/esm/accountSummary.js +36 -18
  81. package/dist/esm/accountSummary.js.map +1 -1
  82. package/dist/esm/index.d.ts +2 -0
  83. package/dist/esm/index.d.ts.map +1 -1
  84. package/dist/esm/index.js +1 -0
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/services/activeAssetData.d.ts +13 -0
  87. package/dist/esm/services/activeAssetData.d.ts.map +1 -0
  88. package/dist/esm/services/activeAssetData.js +10 -0
  89. package/dist/esm/services/activeAssetData.js.map +1 -0
  90. package/dist/esm/services/getAccount.d.ts.map +1 -1
  91. package/dist/esm/services/getAccount.js +19 -29
  92. package/dist/esm/services/getAccount.js.map +1 -1
  93. package/dist/esm/services/getAvailableToTrade.d.ts +26 -0
  94. package/dist/esm/services/getAvailableToTrade.d.ts.map +1 -0
  95. package/dist/esm/services/getAvailableToTrade.js +42 -0
  96. package/dist/esm/services/getAvailableToTrade.js.map +1 -0
  97. package/dist/esm/services/getMarketSettings.d.ts +1 -1
  98. package/dist/esm/services/getMarketSettings.d.ts.map +1 -1
  99. package/dist/esm/services/getMarketSettings.js +3 -7
  100. package/dist/esm/services/getMarketSettings.js.map +1 -1
  101. package/dist/esm/services/getOrder.d.ts.map +1 -1
  102. package/dist/esm/services/getOrder.js +7 -3
  103. package/dist/esm/services/getOrder.js.map +1 -1
  104. package/dist/esm/services/getOrders.d.ts.map +1 -1
  105. package/dist/esm/services/getOrders.js +6 -34
  106. package/dist/esm/services/getOrders.js.map +1 -1
  107. package/dist/esm/services/getWithdrawableBalances.d.ts +24 -0
  108. package/dist/esm/services/getWithdrawableBalances.d.ts.map +1 -0
  109. package/dist/esm/services/getWithdrawableBalances.js +39 -0
  110. package/dist/esm/services/getWithdrawableBalances.js.map +1 -0
  111. package/dist/esm/types/account.d.ts +21 -8
  112. package/dist/esm/types/account.d.ts.map +1 -1
  113. package/dist/esm/types/account.js.map +1 -1
  114. package/dist/esm/types/ws.d.ts +10 -4
  115. package/dist/esm/types/ws.d.ts.map +1 -1
  116. package/dist/esm/utils/abstractionMode.d.ts +1 -1
  117. package/dist/esm/utils/abstractionMode.d.ts.map +1 -1
  118. package/dist/esm/utils/abstractionMode.js.map +1 -1
  119. package/dist/esm/utils/explorer.d.ts +9 -1
  120. package/dist/esm/utils/explorer.d.ts.map +1 -1
  121. package/dist/esm/utils/explorer.js +34 -1
  122. package/dist/esm/utils/explorer.js.map +1 -1
  123. package/dist/esm/utils/index.d.ts +3 -1
  124. package/dist/esm/utils/index.d.ts.map +1 -1
  125. package/dist/esm/utils/index.js +3 -1
  126. package/dist/esm/utils/index.js.map +1 -1
  127. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  128. package/dist/esm/utils/mapPosition.js +1 -4
  129. package/dist/esm/utils/mapPosition.js.map +1 -1
  130. package/dist/esm/utils/spotCollateral.d.ts +4 -4
  131. package/dist/esm/utils/spotCollateral.d.ts.map +1 -1
  132. package/dist/esm/utils/spotCollateral.js +4 -23
  133. package/dist/esm/utils/spotCollateral.js.map +1 -1
  134. package/dist/esm/utils/transferMargin.d.ts +12 -8
  135. package/dist/esm/utils/transferMargin.d.ts.map +1 -1
  136. package/dist/esm/utils/transferMargin.js +30 -18
  137. package/dist/esm/utils/transferMargin.js.map +1 -1
  138. package/dist/esm/utils/venueTotals.d.ts +26 -0
  139. package/dist/esm/utils/venueTotals.d.ts.map +1 -0
  140. package/dist/esm/utils/venueTotals.js +26 -0
  141. package/dist/esm/utils/venueTotals.js.map +1 -0
  142. package/dist/esm/utils/withdrawableBalances.d.ts +16 -0
  143. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -0
  144. package/dist/esm/utils/withdrawableBalances.js +50 -0
  145. package/dist/esm/utils/withdrawableBalances.js.map +1 -0
  146. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +4 -1
  147. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  148. package/dist/esm/websocket/HyperliquidWsProvider.js +101 -35
  149. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  150. package/dist/types/HyperliquidProvider.d.ts.map +1 -1
  151. package/dist/types/accountSummary.d.ts +4 -3
  152. package/dist/types/accountSummary.d.ts.map +1 -1
  153. package/dist/types/index.d.ts +2 -0
  154. package/dist/types/index.d.ts.map +1 -1
  155. package/dist/types/services/activeAssetData.d.ts +13 -0
  156. package/dist/types/services/activeAssetData.d.ts.map +1 -0
  157. package/dist/types/services/getAccount.d.ts.map +1 -1
  158. package/dist/types/services/getAvailableToTrade.d.ts +26 -0
  159. package/dist/types/services/getAvailableToTrade.d.ts.map +1 -0
  160. package/dist/types/services/getMarketSettings.d.ts +1 -1
  161. package/dist/types/services/getMarketSettings.d.ts.map +1 -1
  162. package/dist/types/services/getOrder.d.ts.map +1 -1
  163. package/dist/types/services/getOrders.d.ts.map +1 -1
  164. package/dist/types/services/getWithdrawableBalances.d.ts +24 -0
  165. package/dist/types/services/getWithdrawableBalances.d.ts.map +1 -0
  166. package/dist/types/types/account.d.ts +21 -8
  167. package/dist/types/types/account.d.ts.map +1 -1
  168. package/dist/types/types/ws.d.ts +10 -4
  169. package/dist/types/types/ws.d.ts.map +1 -1
  170. package/dist/types/utils/abstractionMode.d.ts +1 -1
  171. package/dist/types/utils/abstractionMode.d.ts.map +1 -1
  172. package/dist/types/utils/explorer.d.ts +9 -1
  173. package/dist/types/utils/explorer.d.ts.map +1 -1
  174. package/dist/types/utils/index.d.ts +3 -1
  175. package/dist/types/utils/index.d.ts.map +1 -1
  176. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  177. package/dist/types/utils/spotCollateral.d.ts +4 -4
  178. package/dist/types/utils/spotCollateral.d.ts.map +1 -1
  179. package/dist/types/utils/transferMargin.d.ts +12 -8
  180. package/dist/types/utils/transferMargin.d.ts.map +1 -1
  181. package/dist/types/utils/venueTotals.d.ts +26 -0
  182. package/dist/types/utils/venueTotals.d.ts.map +1 -0
  183. package/dist/types/utils/withdrawableBalances.d.ts +16 -0
  184. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -0
  185. package/dist/types/websocket/HyperliquidWsProvider.d.ts +4 -1
  186. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  187. package/package.json +3 -3
  188. package/src/HyperliquidProvider.ts +28 -2
  189. package/src/accountSummary.ts +64 -19
  190. package/src/index.ts +2 -0
  191. package/src/services/activeAssetData.ts +24 -0
  192. package/src/services/getAccount.ts +37 -52
  193. package/src/services/getAvailableToTrade.ts +72 -0
  194. package/src/services/getMarketSettings.ts +7 -11
  195. package/src/services/getOrder.ts +10 -3
  196. package/src/services/getOrders.ts +5 -56
  197. package/src/services/getWithdrawableBalances.ts +94 -0
  198. package/src/types/account.ts +22 -8
  199. package/src/types/ws.ts +18 -5
  200. package/src/utils/abstractionMode.ts +1 -1
  201. package/src/utils/explorer.ts +51 -2
  202. package/src/utils/index.ts +7 -1
  203. package/src/utils/mapPosition.ts +1 -5
  204. package/src/utils/spotCollateral.ts +4 -25
  205. package/src/utils/transferMargin.ts +31 -20
  206. package/src/utils/venueTotals.ts +48 -0
  207. package/src/utils/withdrawableBalances.ts +69 -0
  208. package/src/websocket/HyperliquidWsProvider.ts +149 -42
  209. package/dist/cjs/websocket/decodeChain.d.ts +0 -11
  210. package/dist/cjs/websocket/decodeChain.d.ts.map +0 -1
  211. package/dist/cjs/websocket/decodeChain.js +0 -37
  212. package/dist/cjs/websocket/decodeChain.js.map +0 -1
  213. package/dist/esm/websocket/decodeChain.d.ts +0 -30
  214. package/dist/esm/websocket/decodeChain.d.ts.map +0 -1
  215. package/dist/esm/websocket/decodeChain.js +0 -44
  216. package/dist/esm/websocket/decodeChain.js.map +0 -1
  217. package/dist/types/websocket/decodeChain.d.ts +0 -30
  218. package/dist/types/websocket/decodeChain.d.ts.map +0 -1
  219. package/src/websocket/decodeChain.ts +0 -54
@@ -1,19 +1,6 @@
1
1
  import type { Balance } from '@lifi/perps-types'
2
2
  import { toWireBig } from './decimal.js'
3
3
 
4
- /**
5
- * Portfolio-margin collateral beyond the category quote assets, keyed by spot
6
- * display symbol with its loan-to-value weight. Hyperliquid credits these
7
- * toward buying power at the given fraction (0.5 for both today). Quote assets
8
- * (USDC/USDT) are already full-value collateral via the category model, so
9
- * they are not listed here.
10
- * https://hyperliquid.gitbook.io/hyperliquid-docs/trading/portfolio-margin
11
- */
12
- const PORTFOLIO_MARGIN_LTV: Readonly<Record<string, number>> = {
13
- HYPE: 0.5,
14
- UBTC: 0.5,
15
- }
16
-
17
4
  /**
18
5
  * Result of {@link partitionSpotBalances}: balances that count toward
19
6
  * collateral and balances treated as ordinary holdings.
@@ -25,15 +12,14 @@ export interface SpotPartition {
25
12
  }
26
13
 
27
14
  /**
28
- * Partition spot balances into collateral and ordinary holdings. Quote assets
29
- * are full-value collateral; portfolio-margin-supported assets retain their
30
- * LTV as `collateralWeight`.
15
+ * Partition spot balances into collateral and ordinary holdings. Category
16
+ * quote assets are collateral; every other token is an ordinary holding.
17
+ * Zero-unit rows are dropped from both lists.
31
18
  * @public
32
19
  */
33
20
  export const partitionSpotBalances = (
34
21
  spotBalances: readonly Balance[],
35
- quoteAssetIds: ReadonlySet<string>,
36
- portfolioMargin: boolean
22
+ quoteAssetIds: ReadonlySet<string>
37
23
  ): SpotPartition => {
38
24
  const collateralBalances: Balance[] = []
39
25
  const balances: Balance[] = []
@@ -43,13 +29,6 @@ export const partitionSpotBalances = (
43
29
  }
44
30
  if (quoteAssetIds.has(balance.asset.id)) {
45
31
  collateralBalances.push(balance)
46
- continue
47
- }
48
- const ltv = portfolioMargin
49
- ? PORTFOLIO_MARGIN_LTV[balance.asset.displaySymbol]
50
- : undefined
51
- if (ltv !== undefined) {
52
- collateralBalances.push({ ...balance, collateralWeight: ltv })
53
32
  } else {
54
33
  balances.push(balance)
55
34
  }
@@ -2,24 +2,27 @@ import { PerpsError } from '@lifi/perps-sdk'
2
2
  import {
3
3
  PerpsErrorCode,
4
4
  type Position,
5
- type PositionMarginConstraints,
6
5
  positionSupportsMarginAdjustment,
6
+ positionSupportsMarginRemoval,
7
7
  } from '@lifi/perps-types'
8
8
  import Big from 'big.js'
9
9
 
10
10
  const NOTIONAL_FLOOR_RATIO = '0.1'
11
- const AMOUNT_INCREMENT = '0.000001'
11
+ const AMOUNT_DECIMALS = 6
12
12
 
13
- function positivePositionAmount(value: string, field: string): Big {
14
- let amount: Big
13
+ function positionAmount(value: string, field: string): Big {
15
14
  try {
16
- amount = new Big(value)
15
+ return new Big(value)
17
16
  } catch {
18
17
  throw new PerpsError(
19
18
  PerpsErrorCode.ValidationError,
20
19
  `Invalid decimal string on Position.${field}: '${value}'`
21
20
  )
22
21
  }
22
+ }
23
+
24
+ function positivePositionAmount(value: string, field: string): Big {
25
+ const amount = positionAmount(value, field)
23
26
  if (amount.lte(0)) {
24
27
  throw new PerpsError(
25
28
  PerpsErrorCode.ValidationError,
@@ -30,36 +33,44 @@ function positivePositionAmount(value: string, field: string): Big {
30
33
  }
31
34
 
32
35
  /**
33
- * Hyperliquid's exact documented transfer-margin requirement:
34
- * `max(initial_margin_required, 0.1 * total_position_value)`.
35
- *
36
- * Strict-isolated markets still return constraints because margin can be
37
- * added; their market capability prevents removal. Cross positions and
38
- * markets without individual margin adjustment return `undefined`.
36
+ * Hyperliquid removable isolated margin:
37
+ * `marginUsed − max(initial_margin_required, 0.1 × total_position_value)`.
38
+ * Hyperliquid isolated `marginUsed` already includes the unrealized PnL of
39
+ * the position, so the PnL is not added again, and it can reach zero or less
40
+ * before liquidation.
39
41
  *
42
+ * @returns `undefined` for cross positions and markets without individual
43
+ * margin adjustment; `'0'` for add-only strict-isolated markets and when
44
+ * nothing is removable.
45
+ * @throws {PerpsError} `ValidationError` when a required `Position` decimal
46
+ * is malformed, or when a size, price or requirement is not positive.
40
47
  * @see https://hyperliquid.gitbook.io/hyperliquid-docs/trading/margining
41
48
  * @public
42
49
  */
43
- export function positionMarginConstraints(
50
+ export function positionRemovableMargin(
44
51
  position: Position
45
- ): PositionMarginConstraints | undefined {
52
+ ): string | undefined {
46
53
  if (!positionSupportsMarginAdjustment(position)) {
47
54
  return undefined
48
55
  }
56
+ if (!positionSupportsMarginRemoval(position)) {
57
+ return '0'
58
+ }
59
+ const marginUsed = positionAmount(position.marginUsed, 'marginUsed')
49
60
  const initialMargin = positivePositionAmount(
50
61
  position.initialMarginRequirement,
51
62
  'initialMarginRequirement'
52
63
  )
53
- const notional = positivePositionAmount(position.size, 'size').times(
54
- positivePositionAmount(position.markPrice, 'markPrice')
55
- )
56
- const notionalFloor = notional.times(NOTIONAL_FLOOR_RATIO)
64
+ const notionalFloor = positivePositionAmount(position.size, 'size')
65
+ .times(positivePositionAmount(position.markPrice, 'markPrice'))
66
+ .times(NOTIONAL_FLOOR_RATIO)
57
67
  const minimumMargin = initialMargin.gt(notionalFloor)
58
68
  ? initialMargin
59
69
  : notionalFloor
60
70
 
61
- return {
62
- minimumMarginRequirement: minimumMargin.toFixed(),
63
- amountIncrement: AMOUNT_INCREMENT,
71
+ const removable = marginUsed.minus(minimumMargin)
72
+ if (removable.lte(0)) {
73
+ return '0'
64
74
  }
75
+ return removable.round(AMOUNT_DECIMALS, Big.roundDown).toFixed()
65
76
  }
@@ -0,0 +1,48 @@
1
+ import type { HyperliquidMarginSummary, Position } from '@lifi/perps-types'
2
+ import Big from 'big.js'
3
+ import { toWireBig } from './decimal.js'
4
+
5
+ /**
6
+ * One perps sub-dex entry {@link perpsTotals} reads. The REST response always
7
+ * carries `marginSummary`; a WebSocket frame may omit it.
8
+ * @public
9
+ */
10
+ export interface DexMarginSummary {
11
+ marginSummary?: HyperliquidMarginSummary
12
+ }
13
+
14
+ /**
15
+ * Sum the venue `marginSummary` totals over every perps sub-dex of an
16
+ * account. `accountValue` is total equity, so it already carries locked
17
+ * margin and unrealized PnL. An entry without a `marginSummary` adds nothing.
18
+ * @public
19
+ */
20
+ export const perpsTotals = (
21
+ dexStates: readonly DexMarginSummary[]
22
+ ): { accountValue: Big; marginUsed: Big } => {
23
+ let accountValue = new Big(0)
24
+ let marginUsed = new Big(0)
25
+ for (const { marginSummary } of dexStates) {
26
+ if (marginSummary === undefined) {
27
+ continue
28
+ }
29
+ accountValue = accountValue.plus(
30
+ toWireBig(marginSummary.accountValue, 'marginSummary.accountValue')
31
+ )
32
+ marginUsed = marginUsed.plus(
33
+ toWireBig(marginSummary.totalMarginUsed, 'marginSummary.totalMarginUsed')
34
+ )
35
+ }
36
+ return { accountValue, marginUsed }
37
+ }
38
+
39
+ /**
40
+ * Sum the unrealized PnL of every position, in quote-asset units.
41
+ * @public
42
+ */
43
+ export const sumUnrealizedPnl = (positions: readonly Position[]): Big =>
44
+ positions.reduce(
45
+ (sum, position) =>
46
+ sum.plus(toWireBig(position.unrealizedPnl, 'position.unrealizedPnl')),
47
+ new Big(0)
48
+ )
@@ -0,0 +1,69 @@
1
+ import type { ProviderWithdrawableBalance } from '@lifi/perps-sdk'
2
+ import type {
3
+ HlAbstractionMode,
4
+ HlClearinghouseState,
5
+ HlSpotClearinghouseState,
6
+ } from '../types/index.js'
7
+ import { isUnifiedAbstraction } from './abstractionMode.js'
8
+ import { assetIsOutcome } from './assetId.js'
9
+ import { toWireBig } from './decimal.js'
10
+
11
+ /**
12
+ * Split a Hyperliquid account's venue figures into the routes a withdrawal
13
+ * draws on. A unified or portfolio-margin account holds its collateral in spot,
14
+ * so each spot token adds the part of `total` that no order or margin holds. A
15
+ * route with nothing left to draw carries no row, and the caller applies the
16
+ * per-asset venue minimum.
17
+ *
18
+ * @param quoteAssetId - `Asset.id` the perps-route row is keyed by.
19
+ * @param withdrawalFee - Flat venue fee in quote-asset units, set on every
20
+ * quote-asset row. Absent leaves every row without a fee.
21
+ * @public
22
+ */
23
+ export const hyperliquidWithdrawableBalances = (
24
+ abstraction: HlAbstractionMode | null,
25
+ state: HlClearinghouseState,
26
+ spotState: HlSpotClearinghouseState,
27
+ quoteAssetId: string,
28
+ withdrawalFee?: string
29
+ ): ProviderWithdrawableBalance[] => {
30
+ const rows: ProviderWithdrawableBalance[] = []
31
+ const fee =
32
+ withdrawalFee === undefined
33
+ ? undefined
34
+ : toWireBig(withdrawalFee, 'providers.withdrawalFeeUsd').toFixed()
35
+ const feeFor = (assetId: string) =>
36
+ fee === undefined || assetId !== quoteAssetId ? {} : { withdrawalFee: fee }
37
+
38
+ if (isUnifiedAbstraction(abstraction)) {
39
+ for (const balance of spotState.balances) {
40
+ if (assetIsOutcome(balance.coin)) {
41
+ continue
42
+ }
43
+ const spot = toWireBig(balance.total, 'spotBalance.total').minus(
44
+ toWireBig(balance.hold, 'spotBalance.hold')
45
+ )
46
+ if (spot.gt(0)) {
47
+ const assetId = String(balance.token)
48
+ rows.push({
49
+ assetId,
50
+ route: 'spot',
51
+ available: spot.toFixed(),
52
+ ...feeFor(assetId),
53
+ })
54
+ }
55
+ }
56
+ }
57
+
58
+ const perps = toWireBig(state.withdrawable, 'clearinghouseState.withdrawable')
59
+ if (perps.gt(0)) {
60
+ rows.push({
61
+ assetId: quoteAssetId,
62
+ route: 'perps',
63
+ available: perps.toFixed(),
64
+ ...feeFor(quoteAssetId),
65
+ })
66
+ }
67
+
68
+ return rows
69
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DecodeChain,
2
3
  getMarketRegistry,
3
4
  isActiveMarket,
4
5
  isActiveOrderStatus,
@@ -10,7 +11,7 @@ import {
10
11
  ReconnectingWebSocket,
11
12
  resolveSubscribeQuote,
12
13
  type SubscriptionListener,
13
- summarizeAccount,
14
+ toAssetDisplay,
14
15
  toPerpsMarketDisplay,
15
16
  WsProviderBase,
16
17
  type WsProviderFactory,
@@ -19,7 +20,6 @@ import {
19
20
  wsLog,
20
21
  } from '@lifi/perps-sdk'
21
22
  import type {
22
- AccountResponse,
23
23
  Balance,
24
24
  MarketContext,
25
25
  Order,
@@ -37,9 +37,11 @@ import {
37
37
  SPOT_MARKET_ID,
38
38
  } from '../constants.js'
39
39
  import type {
40
+ HlActiveAssetData,
40
41
  HlAssetPosition,
41
42
  HlOrderDetail,
42
43
  HlOrderStatusResponse,
44
+ HlSpotClearinghouseState,
43
45
  HlUserFill,
44
46
  HlWsActiveAssetCtxData,
45
47
  HlWsActiveSpotAssetCtxData,
@@ -75,12 +77,13 @@ import {
75
77
  mapOrder,
76
78
  mapPosition,
77
79
  partitionSpotBalances,
80
+ perpsTotals,
78
81
  priceStepToAggregation,
79
82
  spotAssetFromToken,
80
83
  spotBalance,
81
84
  spotPriceById,
85
+ sumUnrealizedPnl,
82
86
  } from '../utils/index.js'
83
- import { DecodeChain } from './decodeChain.js'
84
87
 
85
88
  /** HL's compact `l2` snapshot carries 20 levels per side. */
86
89
  const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20
@@ -88,6 +91,15 @@ const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20
88
91
  const normalizeHlAddress = (address: string): string =>
89
92
  isAddress(address, { strict: false }) ? address.toLowerCase() : address
90
93
 
94
+ /** Venue buying power for the asset the main perps dex settles in. */
95
+ const availableAfterMaintenance = (
96
+ spotState: { tokenToAvailableAfterMaintenance?: readonly [number, string][] },
97
+ quoteAssetId: string | undefined
98
+ ): string | undefined =>
99
+ spotState.tokenToAvailableAfterMaintenance?.find(
100
+ ([token]) => String(token) === quoteAssetId
101
+ )?.[1]
102
+
91
103
  /**
92
104
  * `WsProviderFactory` constructor for Hyperliquid — pass to
93
105
  * `new PerpsWsClient(client, { wsProviders: { hyperliquid: hyperliquidWsProvider() } })`.
@@ -147,11 +159,16 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
147
159
  {
148
160
  releaseSpot: Promise<() => void>
149
161
  spot?: { collateralBalances: Balance[]; balances: Balance[] }
162
+ availableMargin?: string
163
+ availableMarginPending?: boolean
150
164
  }
151
165
  >()
152
166
  // Kept outside the pipeline entry: the first clearinghouse frame lands
153
167
  // while the mode read is still pending, before the pipeline exists.
154
- private readonly latestPositionsByUser = new Map<string, Position[]>()
168
+ private readonly latestPerpsByUser = new Map<
169
+ string,
170
+ { positions: Position[]; marginUsed: string }
171
+ >()
155
172
  private readonly heldSummaryByUser = new Map<
156
173
  string,
157
174
  {
@@ -273,7 +290,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
273
290
  (sub.channel === 'marketContext' ||
274
291
  sub.channel === 'orderbook' ||
275
292
  sub.channel === 'candle' ||
276
- sub.channel === 'trades')
293
+ sub.channel === 'trades' ||
294
+ sub.channel === 'availableToTrade')
277
295
  ) {
278
296
  this.registry.requireActive(sub.marketId)
279
297
  }
@@ -331,7 +349,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
331
349
  // subscription so a resubscribe reflects an account-mode change.
332
350
  this.abstractionByUser.delete(user)
333
351
  this.heldSummaryByUser.delete(user)
334
- this.latestPositionsByUser.delete(user)
352
+ this.latestPerpsByUser.delete(user)
335
353
  this.syncUnifiedPipeline(user, user, null)
336
354
  this.unregisterSub(wireKey)
337
355
  this.rws.send(
@@ -506,6 +524,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
506
524
  return `accountSummary:${sub.address.toLowerCase()}`
507
525
  case 'spotBalances':
508
526
  return `spotState:${sub.address.toLowerCase()}`
527
+ case 'availableToTrade':
528
+ return `activeAssetData:${sub.address.toLowerCase()}:${sub.marketId}`
509
529
  }
510
530
  }
511
531
 
@@ -581,6 +601,12 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
581
601
  }
582
602
  case 'spotBalances':
583
603
  return { type: 'spotState', user: normalizeHlAddress(sub.address) }
604
+ case 'availableToTrade':
605
+ return {
606
+ type: 'activeAssetData',
607
+ user: normalizeHlAddress(sub.address),
608
+ coin: sub.marketId,
609
+ }
584
610
  }
585
611
  }
586
612
 
@@ -656,6 +682,9 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
656
682
  case 'spotState':
657
683
  this.handleSpotState(msg.data as HlWsSpotStateData)
658
684
  break
685
+ case 'activeAssetData':
686
+ this.handleActiveAssetData(msg.data as HlActiveAssetData)
687
+ break
659
688
  }
660
689
  } catch (error) {
661
690
  wsLog.handlerFailure(this.providerKey, error)
@@ -1130,30 +1159,26 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1130
1159
  data: positions,
1131
1160
  })
1132
1161
 
1133
- this.latestPositionsByUser.set(data.user.toLowerCase(), positions)
1134
- this.emitUnifiedSummary(data.user.toLowerCase())
1135
-
1136
- let accountValue = 0
1137
- let marginUsed = 0
1138
- for (const [, state] of data.clearinghouseStates) {
1139
- accountValue +=
1140
- Number.parseFloat(state.marginSummary?.accountValue ?? '') || 0
1141
- marginUsed +=
1142
- Number.parseFloat(state.marginSummary?.totalMarginUsed ?? '') || 0
1143
- }
1144
- const unrealizedPnl = positions.reduce(
1145
- (sum, p) => sum + (Number.parseFloat(p.unrealizedPnl) || 0),
1146
- 0
1162
+ const { accountValue, marginUsed } = perpsTotals(
1163
+ data.clearinghouseStates.map(([, state]) => state)
1147
1164
  )
1165
+
1166
+ const key = data.user.toLowerCase()
1167
+ this.latestPerpsByUser.set(key, {
1168
+ positions,
1169
+ marginUsed: marginUsed.toFixed(),
1170
+ })
1171
+ this.emitUnifiedSummary(key)
1172
+
1148
1173
  // Equity semantics, matching the REST summary: `accountValue` already
1149
1174
  // carries locked margin and unrealized PnL. Spot balances stream apart,
1150
1175
  // so this portfolio value covers perps equity only — which is why the
1151
1176
  // frame is gated on the abstraction mode below.
1152
1177
  this.emitSummaryIfModeAllows(data.user, {
1153
- portfolioValue: accountValue.toString(),
1154
- availableMargin: (accountValue - marginUsed).toString(),
1155
- marginUsed: marginUsed.toString(),
1156
- unrealizedPnl: unrealizedPnl.toString(),
1178
+ portfolioValue: accountValue.toFixed(),
1179
+ availableMargin: accountValue.minus(marginUsed).toFixed(),
1180
+ marginUsed: marginUsed.toFixed(),
1181
+ unrealizedPnl: sumUnrealizedPnl(positions).toFixed(),
1157
1182
  })
1158
1183
  }
1159
1184
 
@@ -1219,24 +1244,32 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1219
1244
 
1220
1245
  private emitUnifiedSummary(key: string) {
1221
1246
  const pipeline = this.unifiedSummaryByUser.get(key)
1222
- const positions = this.latestPositionsByUser.get(key)
1247
+ const perps = this.latestPerpsByUser.get(key)
1223
1248
  // Both envelopes must have arrived: a spot-only summary would report
1224
1249
  // zero margin used against real positions.
1225
- if (pipeline?.spot === undefined || positions === undefined) {
1250
+ if (
1251
+ pipeline?.spot === undefined ||
1252
+ pipeline.availableMargin === undefined ||
1253
+ perps === undefined
1254
+ ) {
1226
1255
  return
1227
1256
  }
1228
- const summary = summarizeAccount(
1229
- // summarizeAccount reads only the two balance lists.
1230
- {
1231
- collateralBalances: pipeline.spot.collateralBalances,
1232
- balances: pipeline.spot.balances,
1233
- } as AccountResponse,
1234
- positions,
1235
- 'gross'
1257
+ const portfolioValue = [
1258
+ ...pipeline.spot.collateralBalances,
1259
+ ...pipeline.spot.balances,
1260
+ ].reduce(
1261
+ (sum, balance) =>
1262
+ sum.plus(toWireBig(balance.valueUsd, 'spotBalance.valueUsd')),
1263
+ new Big(0)
1236
1264
  )
1237
1265
  this.emit(`accountSummary:${key}`, {
1238
1266
  channel: 'accountSummary',
1239
- data: summary,
1267
+ data: {
1268
+ portfolioValue: portfolioValue.toFixed(),
1269
+ availableMargin: pipeline.availableMargin,
1270
+ marginUsed: perps.marginUsed,
1271
+ unrealizedPnl: sumUnrealizedPnl(perps.positions).toFixed(),
1272
+ },
1240
1273
  })
1241
1274
  }
1242
1275
 
@@ -1334,6 +1367,27 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1334
1367
  )
1335
1368
  }
1336
1369
 
1370
+ private handleActiveAssetData(data: HlActiveAssetData) {
1371
+ const market = this.registry?.get(data.coin)
1372
+ if (market === undefined) {
1373
+ throw new PerpsError(
1374
+ PerpsErrorCode.ValidationError,
1375
+ `[${this.providerKey}] activeAssetData frame names unknown market '${data.coin}'.`
1376
+ )
1377
+ }
1378
+ const [buy, sell] = data.availableToTrade
1379
+ this.emit(`activeAssetData:${data.user.toLowerCase()}:${data.coin}`, {
1380
+ channel: 'availableToTrade',
1381
+ data: {
1382
+ providerId: market.providerId,
1383
+ marketId: market.id,
1384
+ asset: toAssetDisplay(market.quoteAsset),
1385
+ buy,
1386
+ sell,
1387
+ },
1388
+ })
1389
+ }
1390
+
1337
1391
  private handleSpotState(data: HlWsSpotStateData) {
1338
1392
  const user = data.user.toLowerCase()
1339
1393
  const markets = this.registry?.activeMarkets ?? []
@@ -1360,20 +1414,66 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1360
1414
  const pipeline = this.unifiedSummaryByUser.get(user)
1361
1415
  if (pipeline !== undefined) {
1362
1416
  const quoteAssetIds = new Set(markets.map((m) => m.quoteAsset.id))
1363
- const entry = this.abstractionByUser.get(user)
1364
- const portfolioMargin =
1365
- entry !== undefined &&
1366
- entry !== 'pending' &&
1367
- entry.mode === HlAbstractionMode.PORTFOLIO_MARGIN
1368
1417
  // Same partition as getAccount so REST and WS agree on collateral.
1369
1418
  pipeline.spot = partitionSpotBalances(
1370
1419
  rows.map(({ balance }) => balance),
1371
- quoteAssetIds,
1372
- portfolioMargin
1420
+ quoteAssetIds
1373
1421
  )
1422
+ const settlementAssetId = markets.find(
1423
+ (m) => m.categoryId === this.providerKey
1424
+ )?.quoteAsset.id
1425
+ const available = availableAfterMaintenance(
1426
+ data.spotState,
1427
+ settlementAssetId
1428
+ )
1429
+ if (available === undefined) {
1430
+ this.fetchAvailableAfterMaintenance(user, data.user, settlementAssetId)
1431
+ } else {
1432
+ pipeline.availableMargin = available
1433
+ }
1374
1434
  this.emitUnifiedSummary(user)
1375
1435
  }
1376
1436
  }
1437
+
1438
+ /** Read the venue buying power over REST when the spot frame omits it. */
1439
+ private fetchAvailableAfterMaintenance(
1440
+ key: string,
1441
+ user: string,
1442
+ quoteAssetId: string | undefined
1443
+ ) {
1444
+ const client = this.client
1445
+ const pipeline = this.unifiedSummaryByUser.get(key)
1446
+ if (
1447
+ client === undefined ||
1448
+ pipeline === undefined ||
1449
+ pipeline.availableMarginPending
1450
+ ) {
1451
+ return
1452
+ }
1453
+ pipeline.availableMarginPending = true
1454
+ infoRequest<HlSpotClearinghouseState>(
1455
+ DEFAULT_HYPERLIQUID_API_URL,
1456
+ { type: 'spotClearinghouseState', user },
1457
+ hlInfoOptions(client)
1458
+ ).then(
1459
+ (state) => {
1460
+ const current = this.unifiedSummaryByUser.get(key)
1461
+ if (current === undefined) {
1462
+ return
1463
+ }
1464
+ current.availableMarginPending = false
1465
+ current.availableMargin = availableAfterMaintenance(state, quoteAssetId)
1466
+ this.emitUnifiedSummary(key)
1467
+ },
1468
+ (error) => {
1469
+ const current = this.unifiedSummaryByUser.get(key)
1470
+ if (current !== undefined) {
1471
+ current.availableMarginPending = false
1472
+ }
1473
+ wsLog.handlerFailure(this.providerKey, error)
1474
+ }
1475
+ )
1476
+ }
1377
1477
  }
1378
1478
 
1379
1479
  const isObject = (v: unknown): v is Record<string, unknown> =>
@@ -1599,6 +1699,13 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
1599
1699
  isObject(data.spotState) &&
1600
1700
  Array.isArray(data.spotState.balances)
1601
1701
  )
1702
+ case 'activeAssetData':
1703
+ return (
1704
+ typeof data.user === 'string' &&
1705
+ typeof data.coin === 'string' &&
1706
+ Array.isArray(data.availableToTrade) &&
1707
+ data.availableToTrade.length === 2
1708
+ )
1602
1709
  default:
1603
1710
  return true
1604
1711
  }
@@ -1,11 +0,0 @@
1
- export type DecodeChainMode = 'every' | 'latest';
2
- export declare class DecodeChain {
3
- private readonly mode;
4
- private readonly onError;
5
- private tail;
6
- private queued;
7
- constructor(mode: DecodeChainMode, onError: (error: unknown) => void);
8
- push(decode: () => Promise<void>): void;
9
- reset(): void;
10
- }
11
- //# sourceMappingURL=decodeChain.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"decodeChain.d.ts","sourceRoot":"","sources":["../../../src/websocket/decodeChain.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,CAAA;AAQhD,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,IAAI,CAAmC;IAC/C,OAAO,CAAC,MAAM,CAAmC;gBAG9B,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;IAGpD,IAAI,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAwBvC,KAAK,IAAI,IAAI;CAId"}
@@ -1,37 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DecodeChain = void 0;
4
- class DecodeChain {
5
- mode;
6
- onError;
7
- tail = Promise.resolve();
8
- queued;
9
- constructor(mode, onError) {
10
- this.mode = mode;
11
- this.onError = onError;
12
- }
13
- push(decode) {
14
- if (this.mode === 'every') {
15
- this.tail = this.tail.then(decode).catch(this.onError);
16
- return;
17
- }
18
- const drainScheduled = this.queued !== undefined;
19
- this.queued = decode;
20
- if (drainScheduled) {
21
- return;
22
- }
23
- this.tail = this.tail
24
- .then(() => {
25
- const next = this.queued;
26
- this.queued = undefined;
27
- return next?.();
28
- })
29
- .catch(this.onError);
30
- }
31
- reset() {
32
- this.tail = Promise.resolve();
33
- this.queued = undefined;
34
- }
35
- }
36
- exports.DecodeChain = DecodeChain;
37
- //# sourceMappingURL=decodeChain.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"decodeChain.js","sourceRoot":"","sources":["../../../src/websocket/decodeChain.ts"],"names":[],"mappings":";;;AAgBA,MAAa,WAAW;IAKH;IACA;IALX,IAAI,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAA;IACvC,MAAM,CAAmC;IAEjD,YACmB,IAAqB,EACrB,OAAiC;QADjC,SAAI,GAAJ,IAAI,CAAiB;QACrB,YAAO,GAAP,OAAO,CAA0B;IACjD,CAAC;IAEJ,IAAI,CAAC,MAA2B;QAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACtD,OAAM;QACR,CAAC;QACD,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,CAAA;QAChD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,cAAc,EAAE,CAAC;YACnB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI;aAClB,IAAI,CAAC,GAAG,EAAE;YACT,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAA;YACxB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;YACvB,OAAO,IAAI,EAAE,EAAE,CAAA;QACjB,CAAC,CAAC;aACD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxB,CAAC;IAOD,KAAK;QACH,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAA;QAC7B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;IACzB,CAAC;CACF;AArCD,kCAqCC"}
@@ -1,30 +0,0 @@
1
- /**
2
- * Frame semantics of a {@link DecodeChain}:
3
- * - `'every'`: every frame decodes and applies in arrival order — required for
4
- * delta-carrying channels, where dropping a frame corrupts downstream state.
5
- * - `'latest'`: frames queued behind an in-flight decode coalesce to the
6
- * newest — for snapshot-like channels, where a newer frame supersedes
7
- * anything still waiting.
8
- */
9
- export type DecodeChainMode = 'every' | 'latest';
10
- /**
11
- * Serializes async WS frame decodes so results apply in arrival order.
12
- * In `'latest'` mode a backlog (e.g. queued behind a main-thread stall)
13
- * collapses to its newest frame instead of replaying every stale decode.
14
- * Decode failures are reported to `onError` and never break the chain.
15
- */
16
- export declare class DecodeChain {
17
- private readonly mode;
18
- private readonly onError;
19
- private tail;
20
- private queued;
21
- constructor(mode: DecodeChainMode, onError: (error: unknown) => void);
22
- push(decode: () => Promise<void>): void;
23
- /**
24
- * Detach on connection teardown: drops the queued latest-mode decode and
25
- * starts subsequent pushes on a fresh chain. A decode already in flight
26
- * still settles.
27
- */
28
- reset(): void;
29
- }
30
- //# sourceMappingURL=decodeChain.d.ts.map