@lifi/perps-sdk-provider-hyperliquid 31.0.1 → 31.0.3

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 (105) hide show
  1. package/README.md +20 -0
  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/constants.d.ts +1 -1
  6. package/dist/cjs/constants.d.ts.map +1 -1
  7. package/dist/cjs/constants.js +2 -2
  8. package/dist/cjs/constants.js.map +1 -1
  9. package/dist/cjs/services/getAvailableToTrade.d.ts.map +1 -1
  10. package/dist/cjs/services/getAvailableToTrade.js +1 -1
  11. package/dist/cjs/services/getAvailableToTrade.js.map +1 -1
  12. package/dist/cjs/services/getFills.d.ts.map +1 -1
  13. package/dist/cjs/services/getFills.js +14 -11
  14. package/dist/cjs/services/getFills.js.map +1 -1
  15. package/dist/cjs/services/getMarketSettings.d.ts.map +1 -1
  16. package/dist/cjs/services/getMarketSettings.js +1 -1
  17. package/dist/cjs/services/getMarketSettings.js.map +1 -1
  18. package/dist/cjs/services/twapSliceFills.d.ts.map +1 -1
  19. package/dist/cjs/services/twapSliceFills.js +3 -3
  20. package/dist/cjs/services/twapSliceFills.js.map +1 -1
  21. package/dist/cjs/types/ws.d.ts +6 -1
  22. package/dist/cjs/types/ws.d.ts.map +1 -1
  23. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  24. package/dist/cjs/utils/mapFill.js +9 -7
  25. package/dist/cjs/utils/mapFill.js.map +1 -1
  26. package/dist/cjs/utils/marketDisplay.d.ts.map +1 -1
  27. package/dist/cjs/utils/marketDisplay.js +2 -1
  28. package/dist/cjs/utils/marketDisplay.js.map +1 -1
  29. package/dist/cjs/utils/spotBalance.d.ts.map +1 -1
  30. package/dist/cjs/utils/spotBalance.js +3 -3
  31. package/dist/cjs/utils/spotBalance.js.map +1 -1
  32. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
  33. package/dist/cjs/utils/withdrawableBalances.js +2 -1
  34. package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
  35. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +4 -0
  36. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  37. package/dist/cjs/websocket/HyperliquidWsProvider.js +61 -33
  38. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  39. package/dist/esm/HyperliquidProvider.d.ts.map +1 -1
  40. package/dist/esm/HyperliquidProvider.js +3 -3
  41. package/dist/esm/HyperliquidProvider.js.map +1 -1
  42. package/dist/esm/constants.d.ts +6 -2
  43. package/dist/esm/constants.d.ts.map +1 -1
  44. package/dist/esm/constants.js +6 -2
  45. package/dist/esm/constants.js.map +1 -1
  46. package/dist/esm/services/getAvailableToTrade.d.ts.map +1 -1
  47. package/dist/esm/services/getAvailableToTrade.js +3 -3
  48. package/dist/esm/services/getAvailableToTrade.js.map +1 -1
  49. package/dist/esm/services/getFills.d.ts +6 -3
  50. package/dist/esm/services/getFills.d.ts.map +1 -1
  51. package/dist/esm/services/getFills.js +19 -13
  52. package/dist/esm/services/getFills.js.map +1 -1
  53. package/dist/esm/services/getMarketSettings.d.ts.map +1 -1
  54. package/dist/esm/services/getMarketSettings.js +3 -3
  55. package/dist/esm/services/getMarketSettings.js.map +1 -1
  56. package/dist/esm/services/twapSliceFills.d.ts.map +1 -1
  57. package/dist/esm/services/twapSliceFills.js +3 -4
  58. package/dist/esm/services/twapSliceFills.js.map +1 -1
  59. package/dist/esm/types/ws.d.ts +7 -1
  60. package/dist/esm/types/ws.d.ts.map +1 -1
  61. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  62. package/dist/esm/utils/mapFill.js +12 -9
  63. package/dist/esm/utils/mapFill.js.map +1 -1
  64. package/dist/esm/utils/marketDisplay.d.ts.map +1 -1
  65. package/dist/esm/utils/marketDisplay.js +3 -2
  66. package/dist/esm/utils/marketDisplay.js.map +1 -1
  67. package/dist/esm/utils/spotBalance.d.ts.map +1 -1
  68. package/dist/esm/utils/spotBalance.js +4 -4
  69. package/dist/esm/utils/spotBalance.js.map +1 -1
  70. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
  71. package/dist/esm/utils/withdrawableBalances.js +3 -2
  72. package/dist/esm/utils/withdrawableBalances.js.map +1 -1
  73. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +10 -0
  74. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  75. package/dist/esm/websocket/HyperliquidWsProvider.js +72 -35
  76. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  77. package/dist/types/HyperliquidProvider.d.ts.map +1 -1
  78. package/dist/types/constants.d.ts +6 -2
  79. package/dist/types/constants.d.ts.map +1 -1
  80. package/dist/types/services/getAvailableToTrade.d.ts.map +1 -1
  81. package/dist/types/services/getFills.d.ts +6 -3
  82. package/dist/types/services/getFills.d.ts.map +1 -1
  83. package/dist/types/services/getMarketSettings.d.ts.map +1 -1
  84. package/dist/types/services/twapSliceFills.d.ts.map +1 -1
  85. package/dist/types/types/ws.d.ts +7 -1
  86. package/dist/types/types/ws.d.ts.map +1 -1
  87. package/dist/types/utils/mapFill.d.ts.map +1 -1
  88. package/dist/types/utils/marketDisplay.d.ts.map +1 -1
  89. package/dist/types/utils/spotBalance.d.ts.map +1 -1
  90. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
  91. package/dist/types/websocket/HyperliquidWsProvider.d.ts +10 -0
  92. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  93. package/package.json +3 -3
  94. package/src/HyperliquidProvider.ts +2 -2
  95. package/src/constants.ts +6 -2
  96. package/src/services/getAvailableToTrade.ts +3 -2
  97. package/src/services/getFills.ts +37 -25
  98. package/src/services/getMarketSettings.ts +3 -2
  99. package/src/services/twapSliceFills.ts +3 -5
  100. package/src/types/ws.ts +8 -1
  101. package/src/utils/mapFill.ts +20 -16
  102. package/src/utils/marketDisplay.ts +3 -7
  103. package/src/utils/spotBalance.ts +4 -3
  104. package/src/utils/withdrawableBalances.ts +3 -2
  105. package/src/websocket/HyperliquidWsProvider.ts +91 -35
@@ -1,9 +1,9 @@
1
1
  import { getMarketRegistry, type SDKRequestOptions } from '@lifi/perps-sdk'
2
2
  import type { Fill, FillsResponse } from '@lifi/perps-types'
3
3
  import type { Address } from 'viem'
4
- import { PROVIDER_KEY } from '../constants.js'
4
+ import { FILL_ROWS_CAP, PROVIDER_KEY } from '../constants.js'
5
5
  import type { HyperliquidContext } from '../context.js'
6
- import type { HlUserFills } from '../types/index.js'
6
+ import type { HlTwapSliceFill, HlUserFills } from '../types/index.js'
7
7
  import { assetIsOutcome, mapFill } from '../utils/index.js'
8
8
  import { hlInfoOptions, infoRequest } from '../utils/infoClient.js'
9
9
  import { requireAccountExists } from './getAccountExists.js'
@@ -20,13 +20,16 @@ export interface GetFillsParams {
20
20
  limit?: number
21
21
  /** Opaque cursor returned in the previous page's `pagination.cursor`. */
22
22
  cursor?: string
23
- /** Inclusive lower bound in milliseconds; selects `userFillsByTime`. */
23
+ /** Inclusive lower bound in milliseconds; selects the `...ByTime` feeds. */
24
24
  startTime?: number
25
- /** Inclusive upper bound in milliseconds; selects `userFillsByTime`. */
25
+ /** Inclusive upper bound in milliseconds; selects the `...ByTime` feeds. */
26
26
  endTime?: number
27
27
  }
28
28
 
29
- /** Fetch retained fills newest-first, using opaque cursors across inclusive time ranges. */
29
+ /**
30
+ * Fetch retained regular and TWAP slice fills newest-first, using opaque
31
+ * cursors across inclusive time ranges.
32
+ */
30
33
  export const getFills = async (
31
34
  { client, apiUrl }: HyperliquidContext,
32
35
  params: GetFillsParams,
@@ -46,30 +49,39 @@ export const getFills = async (
46
49
  params,
47
50
  limit,
48
51
  async (startTime, endTime) => {
49
- const fills = await infoRequest<HlUserFills>(
50
- apiUrl,
51
- useRecent
52
- ? { type: 'userFills', user: params.address }
53
- : {
54
- type: 'userFillsByTime',
55
- user: params.address,
56
- startTime,
57
- endTime,
58
- },
59
- infoOpts
60
- )
52
+ const user = params.address
53
+ const [fills, sliceFills] = await Promise.all([
54
+ infoRequest<HlUserFills>(
55
+ apiUrl,
56
+ useRecent
57
+ ? { type: 'userFills', user }
58
+ : { type: 'userFillsByTime', user, startTime, endTime },
59
+ infoOpts
60
+ ),
61
+ infoRequest<HlTwapSliceFill[]>(
62
+ apiUrl,
63
+ useRecent
64
+ ? { type: 'userTwapSliceFills', user }
65
+ : { type: 'userTwapSliceFillsByTime', user, startTime, endTime },
66
+ infoOpts
67
+ ),
68
+ ])
61
69
  useRecent = false
62
- if (fills.length >= 2000) {
70
+ if (fills.length >= FILL_ROWS_CAP || sliceFills.length >= FILL_ROWS_CAP) {
63
71
  return { items: [], saturated: true }
64
72
  }
65
- const items = fills.flatMap((fill): Fill[] => {
66
- if (assetIsOutcome(fill.coin)) {
67
- return []
73
+ const seen = new Set<number>()
74
+ const items = [...fills, ...sliceFills.map(({ fill }) => fill)].flatMap(
75
+ (fill): Fill[] => {
76
+ if (assetIsOutcome(fill.coin) || seen.has(fill.tid)) {
77
+ return []
78
+ }
79
+ seen.add(fill.tid)
80
+ const market = registry.get(fill.coin)
81
+ const mapped = market && mapFill(fill, market, params.address)
82
+ return mapped ? [mapped] : []
68
83
  }
69
- const market = registry.get(fill.coin)
70
- const mapped = market && mapFill(fill, market, params.address)
71
- return mapped ? [mapped] : []
72
- })
84
+ )
73
85
  return { items, saturated: false }
74
86
  },
75
87
  (fill) => ({
@@ -1,4 +1,5 @@
1
1
  import {
2
+ isSpotCategory,
2
3
  numberToDecimalString,
3
4
  PerpsError,
4
5
  type SDKRequestOptions,
@@ -10,7 +11,7 @@ import {
10
11
  PerpsErrorCode,
11
12
  } from '@lifi/perps-types'
12
13
  import type { Address } from 'viem'
13
- import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
14
+ import { PROVIDER_KEY } from '../constants.js'
14
15
  import type { HyperliquidContext } from '../context.js'
15
16
  import { hlInfoOptions } from '../utils/infoClient.js'
16
17
  import { fetchActiveAssetData } from './activeAssetData.js'
@@ -39,7 +40,7 @@ export const getMarketSettings = async (
39
40
  params: GetMarketSettingsParams,
40
41
  options?: SDKRequestOptions
41
42
  ): Promise<MarketSettings> => {
42
- if (params.market.categoryId === SPOT_MARKET_ID) {
43
+ if (isSpotCategory(params.market.categoryId)) {
43
44
  const error = new PerpsError(
44
45
  PerpsErrorCode.ValidationError,
45
46
  `Hyperliquid market '${params.market.marketId}' is a spot market and carries no leverage setting`
@@ -1,11 +1,9 @@
1
1
  import type { Address } from 'viem'
2
+ import { FILL_ROWS_CAP } from '../constants.js'
2
3
  import type { HlTwapHistoryEntry, HlTwapSliceFill } from '../types/index.js'
3
4
  import { type InfoRequestOptions, infoRequest } from '../utils/infoClient.js'
4
5
  import { historyPage } from './historyPagination.js'
5
6
 
6
- /** Row cap of both `userTwapSliceFills` and `userTwapSliceFillsByTime`. */
7
- const TWAP_SLICE_FILLS_CAP = 2000
8
-
9
7
  /**
10
8
  * Read the slice fills of each `activated` TWAP in `entries`, or none without
11
9
  * a request when no entry is `activated`. `userTwapSliceFills` holds only the
@@ -31,7 +29,7 @@ export const fetchTwapSliceFills = async (
31
29
  { type: 'userTwapSliceFills', user },
32
30
  options
33
31
  )
34
- if (window.length < TWAP_SLICE_FILLS_CAP) {
32
+ if (window.length < FILL_ROWS_CAP) {
35
33
  return window
36
34
  }
37
35
  const oldest = Math.min(...window.map(({ fill }) => fill.time))
@@ -54,7 +52,7 @@ export const fetchTwapSliceFills = async (
54
52
  },
55
53
  options
56
54
  )
57
- return fills.length >= TWAP_SLICE_FILLS_CAP
55
+ return fills.length >= FILL_ROWS_CAP
58
56
  ? { items: [], saturated: true }
59
57
  : { items: fills, saturated: false }
60
58
  },
package/src/types/ws.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import type { DecimalString } from '@lifi/perps-types'
4
4
  import type { HlClearinghouseState, HlSpotBalance } from './account.js'
5
5
  import type { HlCandle, HlL2Book } from './asset.js'
6
- import type { HlUserFill } from './fill.js'
6
+ import type { HlTwapSliceFill, HlUserFill } from './fill.js'
7
7
 
8
8
  type HlWsNumberString = string | number
9
9
 
@@ -185,6 +185,13 @@ export type HlWsUserFillsData = {
185
185
  fills: HlUserFill[]
186
186
  }
187
187
 
188
+ /** TWAP slice fills event. A live frame can omit `isSnapshot`. @public */
189
+ export type HlWsUserTwapSliceFillsData = {
190
+ isSnapshot?: boolean
191
+ user: string
192
+ twapSliceFills: HlTwapSliceFill[]
193
+ }
194
+
188
195
  /**
189
196
  * One sub-dex slice of an `allDexsClearinghouseState` frame. `marginSummary`
190
197
  * is absent on a frame that reports positions only, so a reader must treat it
@@ -2,6 +2,7 @@ import {
2
2
  classifyFillFromPosition,
3
3
  ExplorerChainId,
4
4
  explorerTxUrl,
5
+ isSpotCategory,
5
6
  warnSkippedVenueRow,
6
7
  } from '@lifi/perps-sdk'
7
8
  import type { Fill, FillLiquidation, MarketDisplay } from '@lifi/perps-types'
@@ -11,8 +12,10 @@ import {
11
12
  LiquidityRole,
12
13
  OrderSide,
13
14
  } from '@lifi/perps-types'
14
- import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
15
- import type { HlFillLiquidation, HlUserFill } from '../types/index.js'
15
+ import { zeroHash } from 'viem'
16
+ import { PROVIDER_KEY } from '../constants.js'
17
+ import type { HlUserFill } from '../types/index.js'
18
+ import { isFillLiquidatingAccount } from './isFillLiquidatingAccount.js'
16
19
  import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js'
17
20
 
18
21
  /** Re-export the shared fill-position classifier used by Hyperliquid mappings. @public */
@@ -33,13 +36,11 @@ const mapLiquidationMethod = (
33
36
 
34
37
  // Hyperliquid attaches `liquidation` to the fills of both parties.
35
38
  const mapFillLiquidation = (
36
- liquidation: HlFillLiquidation | undefined,
39
+ fill: HlUserFill,
37
40
  address: string
38
41
  ): FillLiquidation | undefined => {
39
- if (
40
- liquidation === undefined ||
41
- liquidation.liquidatedUser.toLowerCase() !== address.toLowerCase()
42
- ) {
42
+ const { liquidation } = fill
43
+ if (liquidation === undefined || !isFillLiquidatingAccount(fill, address)) {
43
44
  return undefined
44
45
  }
45
46
  const method = mapLiquidationMethod(liquidation.method)
@@ -71,10 +72,10 @@ export const mapFill = (
71
72
  // HL charges the builder portion in the same token as the total fee.
72
73
  const feeAsset = fill.feeToken ?? market.quoteAsset.displaySymbol
73
74
  const side = fill.side === 'B' ? OrderSide.BUY : OrderSide.SELL
74
- const liquidation = mapFillLiquidation(fill.liquidation, address)
75
+ const liquidation = mapFillLiquidation(fill, address)
75
76
 
76
77
  return {
77
- ...(market.categoryId === SPOT_MARKET_ID
78
+ ...(isSpotCategory(market.categoryId)
78
79
  ? { realizedPnl: fill.closedPnl }
79
80
  : { realizedPnl: fill.closedPnl === '0' ? null : fill.closedPnl }),
80
81
  id: String(fill.tid),
@@ -95,13 +96,16 @@ export const mapFill = (
95
96
  ? undefined
96
97
  : { amount: fill.builderFee, asset: feeAsset },
97
98
  startPosition: fill.startPosition,
98
- explorerLink: explorerTxUrl(ExplorerChainId.HYPERLIQUID, fill.hash),
99
- classification:
100
- market.categoryId === SPOT_MARKET_ID
101
- ? side === OrderSide.BUY
102
- ? FillClassification.SPOT_BUY
103
- : FillClassification.SPOT_SELL
104
- : classifyFillFromPosition(fill.startPosition, side, size),
99
+ // A TWAP slice fill carries the zero hash: no venue transaction to link.
100
+ explorerLink: explorerTxUrl(
101
+ ExplorerChainId.HYPERLIQUID,
102
+ fill.hash === zeroHash ? undefined : fill.hash
103
+ ),
104
+ classification: isSpotCategory(market.categoryId)
105
+ ? side === OrderSide.BUY
106
+ ? FillClassification.SPOT_BUY
107
+ : FillClassification.SPOT_SELL
108
+ : classifyFillFromPosition(fill.startPosition, side, size),
105
109
  createdAt,
106
110
  ...(liquidation === undefined ? {} : { liquidation }),
107
111
  }
@@ -1,10 +1,6 @@
1
+ import { isSpotCategory } from '@lifi/perps-sdk'
1
2
  import type { Asset, Market } from '@lifi/perps-types'
2
- import {
3
- MAIN_DEX_NAME,
4
- MAIN_MARKET_ID,
5
- PROVIDER_KEY,
6
- SPOT_MARKET_ID,
7
- } from '../constants.js'
3
+ import { MAIN_DEX_NAME, MAIN_MARKET_ID, PROVIDER_KEY } from '../constants.js'
8
4
  import { applyLogoOverride } from './assetLogo.js'
9
5
 
10
6
  /**
@@ -37,7 +33,7 @@ export const coinAsset = (coin: string): Asset =>
37
33
  export const perpsDexNames = (markets: readonly Market[]): string[] => {
38
34
  const hasLiveMarket = new Map<string, boolean>()
39
35
  for (const m of markets) {
40
- if (m.categoryId === SPOT_MARKET_ID) {
36
+ if (isSpotCategory(m.categoryId)) {
41
37
  continue
42
38
  }
43
39
  const name = m.categoryId === MAIN_MARKET_ID ? MAIN_DEX_NAME : m.categoryId
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  isDecimalString,
3
3
  isDecimalStringGreaterThan,
4
+ isSpotCategory,
4
5
  multiplyDecimalString,
5
6
  } from '@lifi/perps-sdk'
6
7
  import type { Asset, Balance, DecimalString, Market } from '@lifi/perps-types'
7
- import { SPOT_MARKET_ID } from '../constants.js'
8
+ import { SPOT_CATEGORY_ID } from '@lifi/perps-types'
8
9
  import type { HlSpotBalance } from '../types/index.js'
9
10
  import { spotLogoURI } from './assetLogo.js'
10
11
  import { coinAsset } from './marketDisplay.js'
@@ -21,7 +22,7 @@ export const spotPriceById = (
21
22
  ): Map<string, DecimalString> => {
22
23
  const map = new Map<string, DecimalString>()
23
24
  for (const m of markets) {
24
- if (m.categoryId === SPOT_MARKET_ID) {
25
+ if (isSpotCategory(m.categoryId)) {
25
26
  const price = priceByMarketId.get(m.id)
26
27
  map.set(m.baseAsset.id, isDecimalString(price) ? price : '0')
27
28
  }
@@ -61,7 +62,7 @@ export const spotBalance = (
61
62
  ): Balance => {
62
63
  const price = priceById.get(asset.id) ?? '0'
63
64
  return {
64
- categoryId: SPOT_MARKET_ID,
65
+ categoryId: SPOT_CATEGORY_ID,
65
66
  asset,
66
67
  units: total,
67
68
  valueUsd: multiplyDecimalString(total, price),
@@ -5,7 +5,8 @@ import {
5
5
  subtractDecimalString,
6
6
  unknownToDecimalString,
7
7
  } from '@lifi/perps-sdk'
8
- import { PROVIDER_KEY, SPOT_MARKET_ID } from '../constants.js'
8
+ import { SPOT_CATEGORY_ID } from '@lifi/perps-types'
9
+ import { PROVIDER_KEY } from '../constants.js'
9
10
  import type {
10
11
  HlAbstractionMode,
11
12
  HlClearinghouseState,
@@ -69,7 +70,7 @@ export const hyperliquidWithdrawableBalances = (
69
70
  const assetId = String(balance.token)
70
71
  const row = {
71
72
  assetId,
72
- categoryId: SPOT_MARKET_ID,
73
+ categoryId: SPOT_CATEGORY_ID,
73
74
  available: spot,
74
75
  ...feeFor(assetId),
75
76
  }
@@ -10,6 +10,7 @@ import {
10
10
  isActiveOrderStatus,
11
11
  isDecimalString,
12
12
  isDecimalStringZero,
13
+ isSpotCategory,
13
14
  type MarketRegistry,
14
15
  PerpsError,
15
16
  type PerpsSDKClient,
@@ -52,7 +53,6 @@ import {
52
53
  DEFAULT_HYPERLIQUID_API_URL,
53
54
  HYPERLIQUID_FEE_TIER_FALLBACK,
54
55
  PROVIDER_KEY,
55
- SPOT_MARKET_ID,
56
56
  } from '../constants.js'
57
57
  import { requireAccountExists } from '../services/getAccountExists.js'
58
58
  import type {
@@ -81,6 +81,7 @@ import type {
81
81
  HlWsSpotStateData,
82
82
  HlWsTrade,
83
83
  HlWsUserFillsData,
84
+ HlWsUserTwapSliceFillsData,
84
85
  } from '../types/index.js'
85
86
  import { HlAbstractionMode } from '../types/index.js'
86
87
  import {
@@ -335,7 +336,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
335
336
  const market = registry.requireActive(sub.marketId)
336
337
  if (
337
338
  sub.channel === 'availableToTrade' &&
338
- market.categoryId === SPOT_MARKET_ID
339
+ isSpotCategory(market.categoryId)
339
340
  ) {
340
341
  const error = new PerpsError(
341
342
  PerpsErrorCode.ValidationError,
@@ -361,37 +362,17 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
361
362
  // Markets context aggregates slower asset-context feeds with fast mid/mark
362
363
  // ticks from `fastAssetCtxs`.
363
364
  if (sub.channel === 'marketsContext') {
364
- const epoch = this.spotWireEpoch
365
- const entries = this.getMarketsContextSubEntries()
366
- let acquired = 0
367
- const release = () => {
368
- if (epoch !== this.spotWireEpoch) {
369
- return
370
- }
371
- for (let index = 0; index < acquired; index++) {
372
- const { subKey, payload } = entries[index]
373
- this.unregisterSub(subKey)
374
- this.rws.send(
375
- JSON.stringify({ method: 'unsubscribe', subscription: payload })
376
- )
377
- }
365
+ return this.openWireEntries(this.getMarketsContextSubEntries(), () => {
378
366
  this.perpCtxBySubDex.clear()
379
367
  this.spotCtxByMarketId = {}
380
368
  this.fastCtxByMarketId = {}
381
369
  this.marketsContextByMarketId = {}
382
- acquired = 0
383
- }
384
- try {
385
- for (const { subKey, payload } of entries) {
386
- await this.registerSub(subKey, payload)
387
- acquired++
388
- }
389
- await this.rws.ready()
390
- return release
391
- } catch (error) {
392
- release()
393
- throw error
394
- }
370
+ })
371
+ }
372
+
373
+ // `fills` merges the regular and the TWAP slice fill feeds of one address.
374
+ if (sub.channel === 'fills') {
375
+ return this.openWireEntries(this.getFillsSubEntries(sub.address))
395
376
  }
396
377
 
397
378
  // The spot wire is shared between the public spotBalances channel and
@@ -529,6 +510,58 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
529
510
  ]
530
511
  }
531
512
 
513
+ /** Sub-key + payload pairs of the `fills` channel: regular and TWAP slice fills. */
514
+ private getFillsSubEntries(
515
+ address: string
516
+ ): Array<{ subKey: string; payload: object }> {
517
+ const user = normalizeHlAddress(address)
518
+ return [
519
+ { subKey: `userFills:${user}`, payload: { type: 'userFills', user } },
520
+ {
521
+ subKey: `userTwapSliceFills:${user}`,
522
+ payload: { type: 'userTwapSliceFills', user },
523
+ },
524
+ ]
525
+ }
526
+
527
+ /**
528
+ * Register each wire entry in order and return a release that unregisters
529
+ * the acquired ones. A failed register releases the acquired entries first.
530
+ * A release after `close()` sends no frame.
531
+ */
532
+ private async openWireEntries(
533
+ entries: Array<{ subKey: string; payload: object }>,
534
+ onRelease?: () => void
535
+ ): Promise<() => void> {
536
+ const epoch = this.spotWireEpoch
537
+ let acquired = 0
538
+ const release = () => {
539
+ if (epoch !== this.spotWireEpoch) {
540
+ return
541
+ }
542
+ for (let index = 0; index < acquired; index++) {
543
+ const { subKey, payload } = entries[index]
544
+ this.unregisterSub(subKey)
545
+ this.rws.send(
546
+ JSON.stringify({ method: 'unsubscribe', subscription: payload })
547
+ )
548
+ }
549
+ acquired = 0
550
+ onRelease?.()
551
+ }
552
+ try {
553
+ for (const { subKey, payload } of entries) {
554
+ await this.registerSub(subKey, payload)
555
+ acquired++
556
+ }
557
+ await this.rws.ready()
558
+ return release
559
+ } catch (error) {
560
+ release()
561
+ throw error
562
+ }
563
+ }
564
+
532
565
  private registerOrderbook(marketId: string, orderbookKey: string): void {
533
566
  let activeKeys = this.orderbookKeysByMarketId.get(marketId)
534
567
  if (activeKeys === undefined) {
@@ -603,7 +636,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
603
636
  case 'orderUpdates':
604
637
  return `orderUpdates:${sub.address.toLowerCase()}`
605
638
  case 'fills':
606
- return `userFills:${sub.address.toLowerCase()}`
639
+ return `fills:${sub.address.toLowerCase()}`
607
640
  case 'positions':
608
641
  return `positions:${sub.address.toLowerCase()}`
609
642
  case 'accountSummary':
@@ -683,6 +716,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
683
716
  case 'orderUpdates':
684
717
  return { type: 'orderUpdates', user: normalizeHlAddress(sub.address) }
685
718
  case 'fills':
719
+ // Handled via getFillsSubEntries in openChannel; never reaches
720
+ // toHlPayload, but TS requires an exhaustive switch.
686
721
  return { type: 'userFills', user: normalizeHlAddress(sub.address) }
687
722
  case 'positions':
688
723
  case 'accountSummary':
@@ -765,6 +800,9 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
765
800
  case 'userFills':
766
801
  this.handleUserFills(msg.data as HlWsUserFillsData)
767
802
  break
803
+ case 'userTwapSliceFills':
804
+ this.handleUserTwapSliceFills(msg.data as HlWsUserTwapSliceFillsData)
805
+ break
768
806
  case 'allDexsClearinghouseState':
769
807
  this.handleAllDexsClearinghouseState(
770
808
  msg.data as HlWsAllDexsClearinghouseStateData
@@ -850,7 +888,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
850
888
  private spotMarketIdBySacKey(): Map<string, string> {
851
889
  const map = new Map<string, string>()
852
890
  for (const market of this.registry?.activeMarkets ?? []) {
853
- if (market.categoryId !== SPOT_MARKET_ID) {
891
+ if (!isSpotCategory(market.categoryId)) {
854
892
  continue
855
893
  }
856
894
  map.set(market.id, market.id)
@@ -1231,22 +1269,38 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
1231
1269
  }
1232
1270
 
1233
1271
  private handleUserFills(data: HlWsUserFillsData) {
1272
+ this.emitFills(data.user, data.isSnapshot, data.fills)
1273
+ }
1274
+
1275
+ private handleUserTwapSliceFills(data: HlWsUserTwapSliceFillsData) {
1276
+ this.emitFills(
1277
+ data.user,
1278
+ data.isSnapshot,
1279
+ data.twapSliceFills.map(({ fill }) => fill)
1280
+ )
1281
+ }
1282
+
1283
+ private emitFills(
1284
+ user: string,
1285
+ isSnapshot: boolean | undefined,
1286
+ fills: HlUserFill[]
1287
+ ) {
1234
1288
  // The snapshot frame carries up to ~2000 historical fills and is replayed
1235
1289
  // on every reconnect via `replaySubs`; emitting them would surface old
1236
1290
  // fills as live events on each network blip. Suppress it to an empty event
1237
1291
  // (fill history is available via REST `getFills`) so only live fills are
1238
1292
  // delivered as events.
1239
- const items = data.isSnapshot
1293
+ const items = isSnapshot
1240
1294
  ? []
1241
- : data.fills.flatMap((f) => {
1295
+ : fills.flatMap((f) => {
1242
1296
  if (assetIsOutcome(f.coin)) {
1243
1297
  return []
1244
1298
  }
1245
1299
  const market = this.registry?.get(f.coin)
1246
- const mapped = market && mapFill(f as HlUserFill, market, data.user)
1300
+ const mapped = market && mapFill(f, market, user)
1247
1301
  return mapped ? [mapped] : []
1248
1302
  })
1249
- this.emit(`userFills:${data.user.toLowerCase()}`, {
1303
+ this.emit(`fills:${user.toLowerCase()}`, {
1250
1304
  channel: 'fills',
1251
1305
  data: items,
1252
1306
  })
@@ -1924,6 +1978,8 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
1924
1978
  return Array.isArray(data)
1925
1979
  case 'userFills':
1926
1980
  return typeof data.user === 'string' && Array.isArray(data.fills)
1981
+ case 'userTwapSliceFills':
1982
+ return typeof data.user === 'string' && Array.isArray(data.twapSliceFills)
1927
1983
  case 'allDexsClearinghouseState':
1928
1984
  return (
1929
1985
  typeof data.user === 'string' && Array.isArray(data.clearinghouseStates)