@lifi/perps-sdk-provider-hyperliquid 30.0.1 → 31.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 (148) hide show
  1. package/README.md +36 -2
  2. package/dist/cjs/HyperliquidProvider.d.ts.map +1 -1
  3. package/dist/cjs/HyperliquidProvider.js +1 -1
  4. package/dist/cjs/HyperliquidProvider.js.map +1 -1
  5. package/dist/cjs/services/getActivity.d.ts.map +1 -1
  6. package/dist/cjs/services/getActivity.js +2 -2
  7. package/dist/cjs/services/getActivity.js.map +1 -1
  8. package/dist/cjs/services/getFills.js +1 -1
  9. package/dist/cjs/services/getFills.js.map +1 -1
  10. package/dist/cjs/services/getOrder.d.ts.map +1 -1
  11. package/dist/cjs/services/getOrder.js +4 -1
  12. package/dist/cjs/services/getOrder.js.map +1 -1
  13. package/dist/cjs/services/getOrders.d.ts +1 -1
  14. package/dist/cjs/services/getOrders.d.ts.map +1 -1
  15. package/dist/cjs/services/getOrders.js +7 -1
  16. package/dist/cjs/services/getOrders.js.map +1 -1
  17. package/dist/cjs/services/twapSliceFills.d.ts +5 -0
  18. package/dist/cjs/services/twapSliceFills.d.ts.map +1 -0
  19. package/dist/cjs/services/twapSliceFills.js +37 -0
  20. package/dist/cjs/services/twapSliceFills.js.map +1 -0
  21. package/dist/cjs/types/account.d.ts +1 -1
  22. package/dist/cjs/types/account.d.ts.map +1 -1
  23. package/dist/cjs/types/account.js.map +1 -1
  24. package/dist/cjs/types/fill.d.ts +4 -0
  25. package/dist/cjs/types/fill.d.ts.map +1 -1
  26. package/dist/cjs/types/ledger.d.ts +1 -1
  27. package/dist/cjs/types/ledger.d.ts.map +1 -1
  28. package/dist/cjs/utils/isFillLiquidatingAccount.d.ts +3 -0
  29. package/dist/cjs/utils/isFillLiquidatingAccount.d.ts.map +1 -0
  30. package/dist/cjs/utils/isFillLiquidatingAccount.js +6 -0
  31. package/dist/cjs/utils/isFillLiquidatingAccount.js.map +1 -0
  32. package/dist/cjs/utils/liquidation.d.ts +3 -2
  33. package/dist/cjs/utils/liquidation.d.ts.map +1 -1
  34. package/dist/cjs/utils/liquidation.js +21 -4
  35. package/dist/cjs/utils/liquidation.js.map +1 -1
  36. package/dist/cjs/utils/mapActivity.d.ts.map +1 -1
  37. package/dist/cjs/utils/mapActivity.js +2 -2
  38. package/dist/cjs/utils/mapActivity.js.map +1 -1
  39. package/dist/cjs/utils/mapFill.d.ts +1 -1
  40. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  41. package/dist/cjs/utils/mapFill.js +24 -2
  42. package/dist/cjs/utils/mapFill.js.map +1 -1
  43. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  44. package/dist/cjs/utils/mapOrder.js +18 -6
  45. package/dist/cjs/utils/mapOrder.js.map +1 -1
  46. package/dist/cjs/utils/mapPosition.js +1 -1
  47. package/dist/cjs/utils/mapPosition.js.map +1 -1
  48. package/dist/cjs/utils/twapHistory.d.ts +2 -1
  49. package/dist/cjs/utils/twapHistory.d.ts.map +1 -1
  50. package/dist/cjs/utils/twapHistory.js +33 -1
  51. package/dist/cjs/utils/twapHistory.js.map +1 -1
  52. package/dist/cjs/websocket/HyperliquidWsProvider.js +1 -1
  53. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  54. package/dist/esm/HyperliquidProvider.d.ts.map +1 -1
  55. package/dist/esm/HyperliquidProvider.js +1 -1
  56. package/dist/esm/HyperliquidProvider.js.map +1 -1
  57. package/dist/esm/services/getActivity.d.ts.map +1 -1
  58. package/dist/esm/services/getActivity.js +2 -2
  59. package/dist/esm/services/getActivity.js.map +1 -1
  60. package/dist/esm/services/getFills.js +1 -1
  61. package/dist/esm/services/getFills.js.map +1 -1
  62. package/dist/esm/services/getOrder.d.ts.map +1 -1
  63. package/dist/esm/services/getOrder.js +5 -2
  64. package/dist/esm/services/getOrder.js.map +1 -1
  65. package/dist/esm/services/getOrders.d.ts +1 -1
  66. package/dist/esm/services/getOrders.d.ts.map +1 -1
  67. package/dist/esm/services/getOrders.js +8 -2
  68. package/dist/esm/services/getOrders.js.map +1 -1
  69. package/dist/esm/services/twapSliceFills.d.ts +11 -0
  70. package/dist/esm/services/twapSliceFills.d.ts.map +1 -0
  71. package/dist/esm/services/twapSliceFills.js +41 -0
  72. package/dist/esm/services/twapSliceFills.js.map +1 -0
  73. package/dist/esm/types/account.d.ts +5 -2
  74. package/dist/esm/types/account.d.ts.map +1 -1
  75. package/dist/esm/types/account.js.map +1 -1
  76. package/dist/esm/types/fill.d.ts +11 -0
  77. package/dist/esm/types/fill.d.ts.map +1 -1
  78. package/dist/esm/types/ledger.d.ts +1 -1
  79. package/dist/esm/types/ledger.d.ts.map +1 -1
  80. package/dist/esm/utils/isFillLiquidatingAccount.d.ts +8 -0
  81. package/dist/esm/utils/isFillLiquidatingAccount.d.ts.map +1 -0
  82. package/dist/esm/utils/isFillLiquidatingAccount.js +7 -0
  83. package/dist/esm/utils/isFillLiquidatingAccount.js.map +1 -0
  84. package/dist/esm/utils/liquidation.d.ts +13 -12
  85. package/dist/esm/utils/liquidation.d.ts.map +1 -1
  86. package/dist/esm/utils/liquidation.js +33 -16
  87. package/dist/esm/utils/liquidation.js.map +1 -1
  88. package/dist/esm/utils/mapActivity.d.ts.map +1 -1
  89. package/dist/esm/utils/mapActivity.js +2 -2
  90. package/dist/esm/utils/mapActivity.js.map +1 -1
  91. package/dist/esm/utils/mapFill.d.ts +4 -5
  92. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  93. package/dist/esm/utils/mapFill.js +29 -11
  94. package/dist/esm/utils/mapFill.js.map +1 -1
  95. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  96. package/dist/esm/utils/mapOrder.js +18 -6
  97. package/dist/esm/utils/mapOrder.js.map +1 -1
  98. package/dist/esm/utils/mapPosition.d.ts +1 -1
  99. package/dist/esm/utils/mapPosition.js +2 -2
  100. package/dist/esm/utils/mapPosition.js.map +1 -1
  101. package/dist/esm/utils/twapHistory.d.ts +7 -1
  102. package/dist/esm/utils/twapHistory.d.ts.map +1 -1
  103. package/dist/esm/utils/twapHistory.js +36 -0
  104. package/dist/esm/utils/twapHistory.js.map +1 -1
  105. package/dist/esm/websocket/HyperliquidWsProvider.js +1 -1
  106. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  107. package/dist/types/HyperliquidProvider.d.ts.map +1 -1
  108. package/dist/types/services/getActivity.d.ts.map +1 -1
  109. package/dist/types/services/getOrder.d.ts.map +1 -1
  110. package/dist/types/services/getOrders.d.ts +1 -1
  111. package/dist/types/services/getOrders.d.ts.map +1 -1
  112. package/dist/types/services/twapSliceFills.d.ts +11 -0
  113. package/dist/types/services/twapSliceFills.d.ts.map +1 -0
  114. package/dist/types/types/account.d.ts +5 -2
  115. package/dist/types/types/account.d.ts.map +1 -1
  116. package/dist/types/types/fill.d.ts +11 -0
  117. package/dist/types/types/fill.d.ts.map +1 -1
  118. package/dist/types/types/ledger.d.ts +1 -1
  119. package/dist/types/types/ledger.d.ts.map +1 -1
  120. package/dist/types/utils/isFillLiquidatingAccount.d.ts +8 -0
  121. package/dist/types/utils/isFillLiquidatingAccount.d.ts.map +1 -0
  122. package/dist/types/utils/liquidation.d.ts +13 -12
  123. package/dist/types/utils/liquidation.d.ts.map +1 -1
  124. package/dist/types/utils/mapActivity.d.ts.map +1 -1
  125. package/dist/types/utils/mapFill.d.ts +4 -5
  126. package/dist/types/utils/mapFill.d.ts.map +1 -1
  127. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  128. package/dist/types/utils/mapPosition.d.ts +1 -1
  129. package/dist/types/utils/twapHistory.d.ts +7 -1
  130. package/dist/types/utils/twapHistory.d.ts.map +1 -1
  131. package/package.json +3 -3
  132. package/src/HyperliquidProvider.ts +1 -6
  133. package/src/services/getActivity.ts +3 -3
  134. package/src/services/getFills.ts +1 -1
  135. package/src/services/getOrder.ts +13 -2
  136. package/src/services/getOrders.ts +12 -3
  137. package/src/services/twapSliceFills.ts +65 -0
  138. package/src/types/account.ts +5 -2
  139. package/src/types/fill.ts +12 -0
  140. package/src/types/ledger.ts +1 -1
  141. package/src/utils/isFillLiquidatingAccount.ts +12 -0
  142. package/src/utils/liquidation.ts +44 -17
  143. package/src/utils/mapActivity.ts +2 -2
  144. package/src/utils/mapFill.ts +41 -13
  145. package/src/utils/mapOrder.ts +18 -6
  146. package/src/utils/mapPosition.ts +2 -2
  147. package/src/utils/twapHistory.ts +46 -1
  148. package/src/websocket/HyperliquidWsProvider.ts +1 -1
@@ -15,8 +15,12 @@ import type {
15
15
  import { withExplorerLinks } from '../utils/explorer.js'
16
16
  import { assetIsOutcome, mapOrder } from '../utils/index.js'
17
17
  import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
18
- import { resolveLatestTwapEntries } from '../utils/twapHistory.js'
18
+ import {
19
+ resolveLatestTwapEntries,
20
+ resolveTwapExecution,
21
+ } from '../utils/twapHistory.js'
19
22
  import { requireAccountExists } from './getAccountExists.js'
23
+ import { fetchTwapSliceFills } from './twapSliceFills.js'
20
24
 
21
25
  /**
22
26
  * Parameters for {@link getOrder}.
@@ -78,9 +82,16 @@ export const getOrder = async (
78
82
  String(entry.twapId) === params.id && !assetIsOutcome(entry.state.coin)
79
83
  )
80
84
  if (twap !== undefined) {
85
+ const sliceFills = await fetchTwapSliceFills(
86
+ apiUrl,
87
+ params.address,
88
+ [twap],
89
+ infoOpts
90
+ )
91
+ const [resolved] = resolveTwapExecution([twap], sliceFills)
81
92
  const registry = getMarketRegistry(client, PROVIDER_KEY)
82
93
  await registry.sync()
83
- const order = mapOrder(twap, registry.require(twap.state.coin))
94
+ const order = mapOrder(resolved, registry.require(twap.state.coin))
84
95
  if (order === undefined) {
85
96
  throw orderNotFound(params.id)
86
97
  }
@@ -5,7 +5,7 @@ import {
5
5
  type ProviderGetOrdersParams,
6
6
  type SDKRequestOptions,
7
7
  } from '@lifi/perps-sdk'
8
- import type { Order, OrdersResponse } from '@lifi/perps-types'
8
+ import { type Order, OrderStatus, type OrdersResponse } from '@lifi/perps-types'
9
9
  import { PROVIDER_KEY } from '../constants.js'
10
10
  import type { HyperliquidContext } from '../context.js'
11
11
  import type {
@@ -17,8 +17,12 @@ import { withExplorerLinks } from '../utils/explorer.js'
17
17
  import { mapOrder, perpsDexNames } from '../utils/index.js'
18
18
  import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
19
19
  import { mapOrderRow } from '../utils/mapOrder.js'
20
- import { resolveLatestTwapEntries } from '../utils/twapHistory.js'
20
+ import {
21
+ resolveLatestTwapEntries,
22
+ resolveTwapExecution,
23
+ } from '../utils/twapHistory.js'
21
24
  import { requireAccountExists } from './getAccountExists.js'
25
+ import { fetchTwapSliceFills } from './twapSliceFills.js'
22
26
 
23
27
  /** Parameters for a lifecycle-filtered Hyperliquid order read. */
24
28
  export type GetOrdersParams = ProviderGetOrdersParams
@@ -72,6 +76,11 @@ export const getOrders = async (
72
76
  infoOpts
73
77
  ),
74
78
  ])
79
+ const latestTwaps = resolveLatestTwapEntries(twaps)
80
+ const sliceFills =
81
+ active || statuses.includes(OrderStatus.FILLED)
82
+ ? await fetchTwapSliceFills(apiUrl, params.address, latestTwaps, infoOpts)
83
+ : []
75
84
  const rows = new Map<string, Order>()
76
85
  const keep = (key: string, order: Order | undefined): void => {
77
86
  if (order !== undefined) {
@@ -117,7 +126,7 @@ export const getOrders = async (
117
126
  }
118
127
  }
119
128
  // TWAP ids and regular order ids occupy separate venue namespaces.
120
- for (const twap of resolveLatestTwapEntries(twaps)) {
129
+ for (const twap of resolveTwapExecution(latestTwaps, sliceFills)) {
121
130
  keep(
122
131
  `twap:${twap.twapId}`,
123
132
  mapOrderRow(twap.state.coin, registry, (market) => mapOrder(twap, market))
@@ -0,0 +1,65 @@
1
+ import type { Address } from 'viem'
2
+ import type { HlTwapHistoryEntry, HlTwapSliceFill } from '../types/index.js'
3
+ import { type InfoRequestOptions, infoRequest } from '../utils/infoClient.js'
4
+ import { historyPage } from './historyPagination.js'
5
+
6
+ /** Row cap of both `userTwapSliceFills` and `userTwapSliceFillsByTime`. */
7
+ const TWAP_SLICE_FILLS_CAP = 2000
8
+
9
+ /**
10
+ * Read the slice fills of each `activated` TWAP in `entries`, or none without
11
+ * a request when no entry is `activated`. `userTwapSliceFills` holds only the
12
+ * newest rows of the account, so a full window that does not reach back to a
13
+ * TWAP start gets its older rows from `userTwapSliceFillsByTime`.
14
+ */
15
+ export const fetchTwapSliceFills = async (
16
+ apiUrl: string,
17
+ user: Address,
18
+ entries: readonly HlTwapHistoryEntry[],
19
+ options?: InfoRequestOptions
20
+ ): Promise<HlTwapSliceFill[]> => {
21
+ const starts = entries.flatMap((entry) =>
22
+ entry.twapId !== undefined && entry.status.status === 'activated'
23
+ ? [entry.state.timestamp]
24
+ : []
25
+ )
26
+ if (starts.length === 0) {
27
+ return []
28
+ }
29
+ const window = await infoRequest<HlTwapSliceFill[]>(
30
+ apiUrl,
31
+ { type: 'userTwapSliceFills', user },
32
+ options
33
+ )
34
+ if (window.length < TWAP_SLICE_FILLS_CAP) {
35
+ return window
36
+ }
37
+ const oldest = Math.min(...window.map(({ fill }) => fill.time))
38
+ const startTime = Math.min(...starts)
39
+ if (startTime > oldest) {
40
+ return window
41
+ }
42
+ // The window can end inside its oldest millisecond, so read that millisecond again.
43
+ const older = await historyPage(
44
+ { startTime, endTime: oldest },
45
+ Number.MAX_SAFE_INTEGER,
46
+ async (rangeStart, rangeEnd) => {
47
+ const fills = await infoRequest<HlTwapSliceFill[]>(
48
+ apiUrl,
49
+ {
50
+ type: 'userTwapSliceFillsByTime',
51
+ user,
52
+ startTime: rangeStart,
53
+ endTime: rangeEnd,
54
+ },
55
+ options
56
+ )
57
+ return fills.length >= TWAP_SLICE_FILLS_CAP
58
+ ? { items: [], saturated: true }
59
+ : { items: fills, saturated: false }
60
+ },
61
+ ({ fill }) => ({ time: fill.time, key: fill.tid }),
62
+ options?.signal
63
+ )
64
+ return [...window.filter(({ fill }) => fill.time > oldest), ...older.items]
65
+ }
@@ -18,8 +18,11 @@ export type HlPosition = {
18
18
  entryPx: string
19
19
  /** Position notional value as a decimal string. */
20
20
  positionValue: string
21
- /** Estimated liquidation price; may be an empty string when unavailable. */
22
- liquidationPx: string
21
+ /**
22
+ * Estimated liquidation price. `null` when no price liquidates the
23
+ * position, which Hyperliquid reports for a cross position.
24
+ */
25
+ liquidationPx: string | null
23
26
  /** Unrealized PnL in quote-asset units. */
24
27
  unrealizedPnl: string
25
28
  /** Margin currently assigned to this position. */
package/src/types/fill.ts CHANGED
@@ -53,3 +53,15 @@ export type HlUserFills = HlUserFill[]
53
53
 
54
54
  /** Fills returned by the time-bounded `userFillsByTime` query. @public */
55
55
  export type HlUserFillsByTime = HlUserFill[]
56
+
57
+ /**
58
+ * One row of the `userTwapSliceFills` or `userTwapSliceFillsByTime` query: a
59
+ * fill that a TWAP slice made. Each query returns at most 2,000 rows:
60
+ * `userTwapSliceFills` the newest, newest first, and `userTwapSliceFillsByTime`
61
+ * the oldest of its inclusive range, oldest first.
62
+ * @public
63
+ */
64
+ export type HlTwapSliceFill = {
65
+ fill: HlUserFill
66
+ twapId: number
67
+ }
@@ -18,7 +18,7 @@ export type HlSpotTransferDelta = {
18
18
  destination: Address
19
19
  fee?: string
20
20
  nativeTokenFee?: string
21
- nonce?: number
21
+ nonce?: number | null
22
22
  }
23
23
 
24
24
  /**
@@ -0,0 +1,12 @@
1
+ import type { HlUserFill } from '../types/index.js'
2
+
3
+ /**
4
+ * Whether the fill liquidates `address`. Hyperliquid attaches `liquidation` to
5
+ * the fills of both parties, so only a fill whose `liquidatedUser` is the
6
+ * address counts. The address compare ignores case.
7
+ */
8
+ export const isFillLiquidatingAccount = (
9
+ fill: Pick<HlUserFill, 'liquidation'>,
10
+ address: string
11
+ ): boolean =>
12
+ fill.liquidation?.liquidatedUser.toLowerCase() === address.toLowerCase()
@@ -16,10 +16,17 @@ import {
16
16
  createSafeFunction,
17
17
  divideDecimalString,
18
18
  estimateLiquidationPrice,
19
+ type LiquidationEstimateParams,
19
20
  numberToDecimalString,
20
21
  PerpsError,
22
+ subtractDecimalString,
21
23
  } from '@lifi/perps-sdk'
22
- import { type DecimalString, PerpsErrorCode } from '@lifi/perps-types'
24
+ import {
25
+ type DecimalString,
26
+ MarginMode,
27
+ PerpsErrorCode,
28
+ PositionSide,
29
+ } from '@lifi/perps-types'
23
30
 
24
31
  /**
25
32
  * Maintenance margin fraction for a Hyperliquid asset: half of the initial
@@ -50,34 +57,54 @@ export const safeCalculateMaintenanceMarginRate = createSafeFunction(
50
57
  )
51
58
 
52
59
  /**
53
- * Liquidation price of a new isolated position with the exact Hyperliquid
54
- * formula. For an existing position, prefer `Position.liquidationPrice` from
55
- * the API.
56
- *
57
- * Formula derivation (isolated margin, single position):
58
- * margin_available = entryPrice × (1 / leverage - mmr)
59
- * liq_price = entryPrice - side × margin_available / (1 - mmr × side)
60
+ * Liquidation price of the isolated position that an order leaves, with the
61
+ * exact Hyperliquid formula: a new position, or the blend of the order into
62
+ * `params.position`. Hyperliquid isolated `marginUsed` includes the
63
+ * unrealized PnL, so the collateral of the position is `marginUsed` minus
64
+ * `unrealizedPnl`. For an existing position, prefer
65
+ * `Position.liquidationPrice` from the API.
60
66
  *
61
- * @param leverage - User-selected leverage (e.g., `'10'`)
62
67
  * @param maxLeverage - Asset's maximum leverage (e.g., 50 for BTC). Sets
63
68
  * mmr = 1 / (2 × maxLeverage).
64
- * @returns The estimate, or `undefined` when `leverage` is at or above
65
- * `2 × maxLeverage`, as {@link estimateLiquidationPrice} documents.
69
+ * @returns The estimate, or `undefined` when the order or the position is
70
+ * cross (the liquidation depends on the whole account), when the order
71
+ * reduces the position, or when `leverage` is at or above `2 × maxLeverage`,
72
+ * as {@link estimateLiquidationPrice} documents.
66
73
  * @throws {PerpsError} `ValidationError` when `maxLeverage` is not a finite
67
74
  * number above zero, or on an input that `estimateLiquidationPrice` rejects.
68
75
  * @public
69
76
  */
70
77
  export function calculateLiquidationPrice(
71
- entryPrice: string,
72
- leverage: string,
73
- isLong: boolean,
78
+ params: LiquidationEstimateParams,
74
79
  maxLeverage: number
75
80
  ): string | undefined {
81
+ const { position } = params
82
+ if (
83
+ params.marginMode === MarginMode.CROSS ||
84
+ position?.marginMode === MarginMode.CROSS ||
85
+ (position !== undefined &&
86
+ (position.side === PositionSide.LONG) !== params.isLong)
87
+ ) {
88
+ return undefined
89
+ }
76
90
  return estimateLiquidationPrice({
77
- entryPrice,
78
- leverage,
79
- isLong,
91
+ entryPrice: params.entryPrice,
92
+ leverage: params.leverage,
93
+ isLong: params.isLong,
80
94
  maintenanceMarginRate: calculateMaintenanceMarginRate(maxLeverage),
95
+ size: params.size,
96
+ ...(position === undefined
97
+ ? {}
98
+ : {
99
+ current: {
100
+ size: position.size,
101
+ entryPrice: position.entryPrice,
102
+ margin: subtractDecimalString(
103
+ position.marginUsed,
104
+ position.unrealizedPnl
105
+ ),
106
+ },
107
+ }),
81
108
  })
82
109
  }
83
110
 
@@ -38,6 +38,7 @@ import {
38
38
  isVaultTransferDelta,
39
39
  isWithdrawDelta,
40
40
  } from '../types/index.js'
41
+ import { isFillLiquidatingAccount } from './isFillLiquidatingAccount.js'
41
42
  import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js'
42
43
 
43
44
  /**
@@ -371,11 +372,10 @@ export const mapLiquidationFills = (
371
372
  resolveMarket: (coin: string) => MarketDisplay | undefined,
372
373
  ledgerHashes: ReadonlySet<string>
373
374
  ): LiquidationActivity[] => {
374
- const queried = queriedAddress.toLowerCase()
375
375
  const byOrder = new Map<number, HlUserFill[]>()
376
376
  for (const fill of fills) {
377
377
  if (
378
- fill.liquidation?.liquidatedUser.toLowerCase() !== queried ||
378
+ !isFillLiquidatingAccount(fill, queriedAddress) ||
379
379
  (fill.hash !== undefined && ledgerHashes.has(fill.hash))
380
380
  ) {
381
381
  continue
@@ -4,29 +4,60 @@ import {
4
4
  explorerTxUrl,
5
5
  warnSkippedVenueRow,
6
6
  } from '@lifi/perps-sdk'
7
- import type { Fill, MarketDisplay } from '@lifi/perps-types'
7
+ import type { Fill, FillLiquidation, MarketDisplay } from '@lifi/perps-types'
8
8
  import {
9
9
  FillClassification,
10
+ LiquidationMethod,
10
11
  LiquidityRole,
11
12
  OrderSide,
12
- OrderType,
13
13
  } from '@lifi/perps-types'
14
14
  import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
15
- import type { HlUserFill } from '../types/index.js'
15
+ import type { HlFillLiquidation, HlUserFill } from '../types/index.js'
16
16
  import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js'
17
17
 
18
18
  /** Re-export the shared fill-position classifier used by Hyperliquid mappings. @public */
19
19
  export { classifyFillFromPosition }
20
20
 
21
- /**
22
- * Spot-ness comes from the resolved market's category, not the coin string —
23
- * HL's canonical spot pair 0 is addressed as `PURR/USDC`, not `@0`. A fill with
24
- * an invalid time gives `undefined`.
21
+ const mapLiquidationMethod = (
22
+ method: string
23
+ ): LiquidationMethod | undefined => {
24
+ switch (method) {
25
+ case 'market':
26
+ return LiquidationMethod.MARKET
27
+ case 'backstop':
28
+ return LiquidationMethod.BACKSTOP
29
+ default:
30
+ return undefined
31
+ }
32
+ }
33
+
34
+ // Hyperliquid attaches `liquidation` to the fills of both parties.
35
+ const mapFillLiquidation = (
36
+ liquidation: HlFillLiquidation | undefined,
37
+ address: string
38
+ ): FillLiquidation | undefined => {
39
+ if (
40
+ liquidation === undefined ||
41
+ liquidation.liquidatedUser.toLowerCase() !== address.toLowerCase()
42
+ ) {
43
+ return undefined
44
+ }
45
+ const method = mapLiquidationMethod(liquidation.method)
46
+ return {
47
+ ...(method === undefined ? {} : { method }),
48
+ markPrice: liquidation.markPx,
49
+ }
50
+ }
51
+
52
+ /** Maps a fill using the market category; the order type stays absent.
53
+ * Invalid times return `undefined`. The venue does not report the order type.
54
+ * @param address - Fill owner; only the liquidated account carries `liquidation`.
25
55
  * @public
26
56
  */
27
57
  export const mapFill = (
28
58
  fill: HlUserFill,
29
- market: MarketDisplay
59
+ market: MarketDisplay,
60
+ address: string
30
61
  ): Fill | undefined => {
31
62
  const size = fill.sz
32
63
  const createdAt = rowTimestampToIsoStringOrUndefined(fill.time)
@@ -40,6 +71,7 @@ export const mapFill = (
40
71
  // HL charges the builder portion in the same token as the total fee.
41
72
  const feeAsset = fill.feeToken ?? market.quoteAsset.displaySymbol
42
73
  const side = fill.side === 'B' ? OrderSide.BUY : OrderSide.SELL
74
+ const liquidation = mapFillLiquidation(fill.liquidation, address)
43
75
 
44
76
  return {
45
77
  ...(market.categoryId === SPOT_MARKET_ID
@@ -50,11 +82,6 @@ export const mapFill = (
50
82
  clientOrderId: fill.cloid,
51
83
  market,
52
84
  side,
53
- // HL fills don't carry the originating order type. A maker fill (crossed:
54
- // false) can only come from a resting order, so it's necessarily a limit;
55
- // a taker fill (crossed: true) may be a market OR an aggressive limit order,
56
- // which the payload can't distinguish, so the type is left undefined.
57
- type: fill.crossed ? undefined : OrderType.LIMIT,
58
85
  size,
59
86
  price: fill.px,
60
87
  liquidity: fill.crossed ? LiquidityRole.TAKER : LiquidityRole.MAKER,
@@ -76,5 +103,6 @@ export const mapFill = (
76
103
  : FillClassification.SPOT_SELL
77
104
  : classifyFillFromPosition(fill.startPosition, side, size),
78
105
  createdAt,
106
+ ...(liquidation === undefined ? {} : { liquidation }),
79
107
  }
80
108
  }
@@ -239,14 +239,20 @@ export const mapOrder = (
239
239
  if (updatedAt === undefined) {
240
240
  return skipOrder(market.id, 'time', raw.time)
241
241
  }
242
+ const hasRemaining =
243
+ safeIsDecimalStringGreaterThan(remainingSize, '0') === true
242
244
  let status: OrderStatus
243
245
  let statusReason: string | undefined
244
246
  switch (raw.status.status) {
245
247
  case 'activated':
246
- status =
247
- safeIsDecimalStringGreaterThan(state.executedSz, '0') === true
248
- ? OrderStatus.PARTIALLY_FILLED
249
- : OrderStatus.OPEN
248
+ if (!hasRemaining) {
249
+ status = OrderStatus.FILLED
250
+ } else {
251
+ status =
252
+ safeIsDecimalStringGreaterThan(state.executedSz, '0') === true
253
+ ? OrderStatus.PARTIALLY_FILLED
254
+ : OrderStatus.OPEN
255
+ }
250
256
  break
251
257
  case 'waitingForTrigger':
252
258
  status = OrderStatus.OPEN
@@ -278,7 +284,7 @@ export const mapOrder = (
278
284
  type: OrderType.TWAP,
279
285
  side: state.side === 'B' ? OrderSide.BUY : OrderSide.SELL,
280
286
  originalSize: state.sz,
281
- remainingSize,
287
+ remainingSize: hasRemaining ? remainingSize : '0',
282
288
  filledSize: state.executedSz,
283
289
  ...(averagePrice === undefined ? {} : { averagePrice }),
284
290
  reduceOnly: state.reduceOnly,
@@ -382,5 +388,11 @@ export const mapOrder = (
382
388
  default:
383
389
  throw venueError(`Unknown Hyperliquid time in force: ${o.tif}`)
384
390
  }
385
- return { ...base, type, price: o.limitPx, timeInForce }
391
+ return {
392
+ ...base,
393
+ type,
394
+ price: o.limitPx,
395
+ timeInForce,
396
+ ...(o.tif === 'LiquidationMarket' ? { isLiquidation: true } : {}),
397
+ }
386
398
  }
@@ -43,7 +43,7 @@ export const isOpenAssetPosition = (ap: HlAssetPosition): boolean =>
43
43
  * Hyperliquid signs funding paid as positive and `accruedFunding` signs it as
44
44
  * negative. A row with an invalid size gives `undefined`; an invalid position
45
45
  * value, a non-positive leverage or an invalid funding leaves the derived field
46
- * absent.
46
+ * absent, and a `null` or empty `liquidationPx` leaves `liquidationPrice` absent.
47
47
  * @public
48
48
  */
49
49
  export const mapPosition = (
@@ -93,7 +93,7 @@ export const mapPosition = (
93
93
  size,
94
94
  entryPrice: pos.entryPx ?? '0',
95
95
  ...(markPrice === undefined ? {} : { markPrice }),
96
- liquidationPrice: pos.liquidationPx || '0',
96
+ ...(pos.liquidationPx ? { liquidationPrice: pos.liquidationPx } : {}),
97
97
  unrealizedPnl: pos.unrealizedPnl,
98
98
  ...(accruedFunding === undefined ? {} : { accruedFunding }),
99
99
  ...(leverage === undefined ? {} : { leverage }),
@@ -1,4 +1,8 @@
1
- import type { HlTwapHistoryEntry } from '../types/index.js'
1
+ import {
2
+ safeAddDecimalStrings,
3
+ safeCalculateNotionalValue,
4
+ } from '@lifi/perps-sdk'
5
+ import type { HlTwapHistoryEntry, HlTwapSliceFill } from '../types/index.js'
2
6
 
3
7
  /**
4
8
  * Keep the newest `twapHistory` row of each TWAP, in first-seen id order. The
@@ -16,3 +20,44 @@ export const resolveLatestTwapEntries = (
16
20
  }
17
21
  return [...latest.values()]
18
22
  }
23
+
24
+ /**
25
+ * Take the executed size and notional of each `activated` row from the slice
26
+ * fills of its `twapId`. The venue writes that row once, at activation. A row
27
+ * with no slice fill, or with a slice fill that does not read, keeps its state.
28
+ */
29
+ export const resolveTwapExecution = (
30
+ entries: readonly HlTwapHistoryEntry[],
31
+ sliceFills: readonly HlTwapSliceFill[]
32
+ ): HlTwapHistoryEntry[] => {
33
+ const fillsByTwap = new Map<number, HlTwapSliceFill['fill'][]>()
34
+ for (const { twapId, fill } of sliceFills) {
35
+ const fills = fillsByTwap.get(twapId)
36
+ if (fills === undefined) {
37
+ fillsByTwap.set(twapId, [fill])
38
+ } else {
39
+ fills.push(fill)
40
+ }
41
+ }
42
+ return entries.map((entry) => {
43
+ const fills =
44
+ entry.status.status === 'activated' && entry.twapId !== undefined
45
+ ? fillsByTwap.get(entry.twapId)
46
+ : undefined
47
+ if (fills === undefined) {
48
+ return entry
49
+ }
50
+ const notionals = fills.map((fill) =>
51
+ safeCalculateNotionalValue(fill.sz, fill.px)
52
+ )
53
+ if (!notionals.every((notional) => notional !== undefined)) {
54
+ return entry
55
+ }
56
+ const executedSz = safeAddDecimalStrings(fills.map((fill) => fill.sz))
57
+ const executedNtl = safeAddDecimalStrings(notionals)
58
+ if (executedSz === undefined || executedNtl === undefined) {
59
+ return entry
60
+ }
61
+ return { ...entry, state: { ...entry.state, executedSz, executedNtl } }
62
+ })
63
+ }
@@ -1243,7 +1243,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1243
1243
  return []
1244
1244
  }
1245
1245
  const market = this.registry?.get(f.coin)
1246
- const mapped = market && mapFill(f as HlUserFill, market)
1246
+ const mapped = market && mapFill(f as HlUserFill, market, data.user)
1247
1247
  return mapped ? [mapped] : []
1248
1248
  })
1249
1249
  this.emit(`userFills:${data.user.toLowerCase()}`, {