@lifi/perps-sdk-provider-hyperliquid 1.0.1 → 1.2.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 (79) hide show
  1. package/dist/cjs/services/getAccount.js +1 -1
  2. package/dist/cjs/services/getAccount.js.map +1 -1
  3. package/dist/cjs/types/asset.d.ts +0 -1
  4. package/dist/cjs/types/asset.d.ts.map +1 -1
  5. package/dist/cjs/types/ws.d.ts +50 -4
  6. package/dist/cjs/types/ws.d.ts.map +1 -1
  7. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts +4 -0
  8. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts.map +1 -0
  9. package/dist/cjs/utils/decodeFastAssetCtxs.js +16 -0
  10. package/dist/cjs/utils/decodeFastAssetCtxs.js.map +1 -0
  11. package/dist/cjs/utils/index.d.ts +2 -1
  12. package/dist/cjs/utils/index.d.ts.map +1 -1
  13. package/dist/cjs/utils/index.js +6 -3
  14. package/dist/cjs/utils/index.js.map +1 -1
  15. package/dist/cjs/utils/mapMarketContext.d.ts +4 -0
  16. package/dist/cjs/utils/mapMarketContext.d.ts.map +1 -0
  17. package/dist/cjs/utils/mapMarketContext.js +23 -0
  18. package/dist/cjs/utils/mapMarketContext.js.map +1 -0
  19. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +21 -5
  20. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  21. package/dist/cjs/websocket/HyperliquidWsProvider.js +291 -60
  22. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  23. package/dist/esm/services/getAccount.js +2 -2
  24. package/dist/esm/services/getAccount.js.map +1 -1
  25. package/dist/esm/types/asset.d.ts +0 -2
  26. package/dist/esm/types/asset.d.ts.map +1 -1
  27. package/dist/esm/types/ws.d.ts +71 -5
  28. package/dist/esm/types/ws.d.ts.map +1 -1
  29. package/dist/esm/utils/decodeFastAssetCtxs.d.ts +12 -0
  30. package/dist/esm/utils/decodeFastAssetCtxs.d.ts.map +1 -0
  31. package/dist/esm/utils/decodeFastAssetCtxs.js +20 -0
  32. package/dist/esm/utils/decodeFastAssetCtxs.js.map +1 -0
  33. package/dist/esm/utils/index.d.ts +2 -1
  34. package/dist/esm/utils/index.d.ts.map +1 -1
  35. package/dist/esm/utils/index.js +2 -1
  36. package/dist/esm/utils/index.js.map +1 -1
  37. package/dist/esm/utils/mapMarket.d.ts +2 -2
  38. package/dist/esm/utils/mapMarket.js +2 -2
  39. package/dist/esm/utils/mapMarketContext.d.ts +10 -0
  40. package/dist/esm/utils/mapMarketContext.d.ts.map +1 -0
  41. package/dist/esm/utils/mapMarketContext.js +25 -0
  42. package/dist/esm/utils/mapMarketContext.js.map +1 -0
  43. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +45 -13
  44. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  45. package/dist/esm/websocket/HyperliquidWsProvider.js +331 -76
  46. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  47. package/dist/types/types/asset.d.ts +0 -2
  48. package/dist/types/types/asset.d.ts.map +1 -1
  49. package/dist/types/types/ws.d.ts +71 -5
  50. package/dist/types/types/ws.d.ts.map +1 -1
  51. package/dist/types/utils/decodeFastAssetCtxs.d.ts +12 -0
  52. package/dist/types/utils/decodeFastAssetCtxs.d.ts.map +1 -0
  53. package/dist/types/utils/index.d.ts +2 -1
  54. package/dist/types/utils/index.d.ts.map +1 -1
  55. package/dist/types/utils/mapMarket.d.ts +2 -2
  56. package/dist/types/utils/mapMarketContext.d.ts +10 -0
  57. package/dist/types/utils/mapMarketContext.d.ts.map +1 -0
  58. package/dist/types/websocket/HyperliquidWsProvider.d.ts +45 -13
  59. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  60. package/package.json +5 -5
  61. package/src/services/getAccount.ts +2 -2
  62. package/src/types/asset.ts +0 -3
  63. package/src/types/ws.ts +71 -5
  64. package/src/utils/decodeFastAssetCtxs.ts +24 -0
  65. package/src/utils/index.ts +5 -1
  66. package/src/utils/mapMarket.ts +2 -2
  67. package/src/utils/mapMarketContext.ts +33 -0
  68. package/src/websocket/HyperliquidWsProvider.ts +406 -86
  69. package/dist/cjs/utils/mapMarketPrice.d.ts +0 -4
  70. package/dist/cjs/utils/mapMarketPrice.d.ts.map +0 -1
  71. package/dist/cjs/utils/mapMarketPrice.js +0 -22
  72. package/dist/cjs/utils/mapMarketPrice.js.map +0 -1
  73. package/dist/esm/utils/mapMarketPrice.d.ts +0 -10
  74. package/dist/esm/utils/mapMarketPrice.d.ts.map +0 -1
  75. package/dist/esm/utils/mapMarketPrice.js +0 -24
  76. package/dist/esm/utils/mapMarketPrice.js.map +0 -1
  77. package/dist/types/utils/mapMarketPrice.d.ts +0 -10
  78. package/dist/types/utils/mapMarketPrice.d.ts.map +0 -1
  79. package/src/utils/mapMarketPrice.ts +0 -33
@@ -12,7 +12,10 @@ import {
12
12
  wsLog,
13
13
  } from '@lifi/perps-sdk'
14
14
  import {
15
+ type MarketContext,
15
16
  type OpenOrder,
17
+ type OrderbookLevel,
18
+ type OrderbookResponse,
16
19
  OrderSide,
17
20
  OrderType,
18
21
  type Subscription,
@@ -23,18 +26,26 @@ import type {
23
26
  HlAssetPosition,
24
27
  HlOrderDetail,
25
28
  HlUserFill,
29
+ HlWsAllDexsAssetCtxsData,
26
30
  HlWsAllDexsClearinghouseStateData,
27
- HlWsAllMidsData,
28
31
  HlWsCandleData,
32
+ HlWsCompressedL2Data,
33
+ HlWsFastAssetCtx,
29
34
  HlWsL2BookData,
35
+ HlWsL2Data,
30
36
  HlWsMessage,
37
+ HlWsPerpAssetCtx,
31
38
  HlWsSpotStateData,
39
+ HlWsTrade,
32
40
  HlWsUserFillsData,
33
41
  } from '../types/index.js'
34
42
  import {
43
+ decodeCompressedJson,
44
+ decodeFastAssetCtxs,
35
45
  isOpenAssetPosition,
36
46
  isTriggerOrder,
37
47
  mapFill,
48
+ mapMarketContext,
38
49
  mapOrderStatus,
39
50
  mapOrderType,
40
51
  mapPosition,
@@ -44,30 +55,27 @@ import {
44
55
  spotPriceById,
45
56
  } from '../utils/index.js'
46
57
 
47
- /** HL's `l2Book` returns at most this many levels per side (slow/default mode). */
58
+ /** HL's compact `l2` snapshot carries 20 levels per side. */
48
59
  const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20
49
60
 
50
61
  /**
51
62
  * `WsProviderFactory` constructor for Hyperliquid — pass to
52
63
  * `new PerpsWsClient(client, { wsProviders: { hyperliquid: hyperliquidWsProvider() } })`.
53
64
  *
54
- * Derives the active sub-DEX set from the `markets` list `PerpsWsClient`
55
- * passes in (the provider's own key and `'spot'` are excluded — they
56
- * aren't sub-DEX subscriptions on the HL wire). Higher-order shape mirrors
57
- * `lighterWsProvider(options)` so the two factories register identically.
65
+ * Higher-order shape mirrors `lighterWsProvider(options)` so the two factories
66
+ * register identically.
58
67
  *
59
68
  * @public
60
69
  */
61
70
  export const hyperliquidWsProvider =
62
71
  (): WsProviderFactory =>
63
- ({ provider, wsUrl, markets, client }) => {
64
- const subDexes = markets.filter((m) => m !== provider && m !== 'spot')
65
- return new HyperliquidWsProvider(wsUrl, provider, subDexes, client)
66
- }
72
+ ({ provider, wsUrl, client }) =>
73
+ new HyperliquidWsProvider(wsUrl, provider, client)
67
74
 
68
75
  /**
69
- * Hyperliquid realtime {@link WsProvider}: multiplexes prices, positions,
70
- * orders, fills and spot balances over a single {@link ReconnectingWebSocket}.
76
+ * Hyperliquid realtime {@link WsProvider}: multiplexes markets context,
77
+ * positions, orders, fills and spot balances over a single
78
+ * {@link ReconnectingWebSocket}.
71
79
  * Construct via {@link hyperliquidWsProvider}.
72
80
  *
73
81
  * `orderUpdates` supports a single address per provider instance: HL delivers
@@ -80,22 +88,26 @@ export const hyperliquidWsProvider =
80
88
  */
81
89
  export class HyperliquidWsProvider extends WsProviderBase<object> {
82
90
  private orderUpdatesKey: string | undefined
83
- private readonly subDexes: string[]
84
91
  private readonly client: PerpsSDKClient | undefined
85
92
  private readonly registry: MarketRegistry | undefined
86
- private midsBySubDex = new Map<string, Record<string, string>>()
87
-
88
- constructor(
89
- wsUrl: string,
90
- providerKey: string,
91
- subDexes: string[],
92
- client?: PerpsSDKClient
93
- ) {
93
+ private perpCtxBySubDex = new Map<string, Record<string, HlWsPerpAssetCtx>>()
94
+ private orderbookKeysByMarketId = new Map<string, Set<string>>()
95
+ private latestOrderbookByMarketId = new Map<string, OrderbookResponse>()
96
+ // Latest mid/mark per `Market.id` from the high-frequency `fastAssetCtxs`
97
+ // feed (all dexes, incl. builder/sub-dex coins). Merged incrementally —
98
+ // frames carry only changed coins — and overlaid onto the slower
99
+ // `allDexsAssetCtxs` context (which supplies oracle/funding/OI/metadata).
100
+ private fastCtxByMarketId: Record<string, HlWsFastAssetCtx> = {}
101
+ // `fastAssetCtxs` payloads are base64 + raw-DEFLATE; decode is async, so
102
+ // chain decodes to apply incremental frames in arrival order.
103
+ private fastDecodeChain: Promise<void> = Promise.resolve()
104
+ private orderbookDecodeChain: Promise<void> = Promise.resolve()
105
+
106
+ constructor(wsUrl: string, providerKey: string, client?: PerpsSDKClient) {
94
107
  super(
95
108
  new ReconnectingWebSocket(wsUrl, { pingPayload: '{"method":"ping"}' }),
96
109
  providerKey
97
110
  )
98
- this.subDexes = subDexes
99
111
  this.client = client
100
112
  this.registry = client && getMarketRegistry(client, providerKey)
101
113
  }
@@ -130,9 +142,11 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
130
142
 
131
143
  await this.registry?.sync()
132
144
 
133
- // Prices require multi-sub-dex allMids subscriptions under one logical key.
134
- if (sub.channel === 'prices') {
135
- const entries = this.getPriceSubEntries()
145
+ // Markets context aggregates the all-dexs perp asset-context feed
146
+ // (oracle, funding, OI, metadata) with the high-frequency `fastAssetCtxs`
147
+ // feed, which supplies fast mid + mark for every market across all dexes.
148
+ if (sub.channel === 'marketsContext') {
149
+ const entries = this.getMarketsContextSubEntries()
136
150
 
137
151
  for (const { subKey, payload } of entries) {
138
152
  await this.registerSub(subKey, payload)
@@ -147,14 +161,21 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
147
161
  JSON.stringify({ method: 'unsubscribe', subscription: payload })
148
162
  )
149
163
  }
150
- this.midsBySubDex.clear()
164
+ this.perpCtxBySubDex.clear()
165
+ this.fastCtxByMarketId = {}
151
166
  }
152
167
  }
153
168
 
154
169
  // All other channels: single WS subscription per key
155
170
  const key = this.toKey(sub)
171
+ if (sub.channel === 'orderbook') {
172
+ this.claimOrderbookKey(sub.marketId, key)
173
+ }
156
174
  const payload = this.toHlPayload(sub)
157
175
  await this.registerSub(key, payload)
176
+ if (sub.channel === 'orderbook') {
177
+ this.registerOrderbook(sub.marketId, key)
178
+ }
158
179
 
159
180
  await this.rws.ready()
160
181
 
@@ -164,8 +185,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
164
185
  this.orderUpdatesKey = undefined
165
186
  }
166
187
  this.rws.send(
167
- JSON.stringify({ method: 'unsubscribe', subscription: payload })
188
+ JSON.stringify({
189
+ method: 'unsubscribe',
190
+ subscription: payload,
191
+ })
168
192
  )
193
+ if (sub.channel === 'orderbook') {
194
+ this.releaseOrderbook(sub.marketId, key)
195
+ }
169
196
  }
170
197
  }
171
198
 
@@ -192,22 +219,69 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
192
219
  this.orderUpdatesKey = key
193
220
  }
194
221
 
195
- /** Build sub-key + payload pairs for each sub-dex allMids subscription. */
196
- private getPriceSubEntries(): Array<{ subKey: string; payload: object }> {
222
+ private claimOrderbookKey(marketId: string, key: string): void {
223
+ const activeKeys = this.orderbookKeysByMarketId.get(marketId)
224
+ if (activeKeys === undefined) {
225
+ return
226
+ }
227
+
228
+ for (const activeKey of [...activeKeys]) {
229
+ if (activeKey === key) {
230
+ continue
231
+ }
232
+ if (!this.closeChannelIfIdle(activeKey)) {
233
+ throw new Error(
234
+ `Hyperliquid supports one orderbook aggregation per market per ` +
235
+ `provider instance. Unsubscribe ${activeKey} before subscribing ${key}.`
236
+ )
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Sub-key + payload pairs for the markets-context aggregation: the all-dexs
243
+ * perp asset-context feed (oracle/funding/OI/metadata) plus the compressed
244
+ * `fastAssetCtxs` feed (high-frequency mid + mark across every dex).
245
+ */
246
+ private getMarketsContextSubEntries(): Array<{
247
+ subKey: string
248
+ payload: object
249
+ }> {
197
250
  return [
198
- {
199
- subKey: 'allMids:default',
200
- payload: { type: 'allMids' },
201
- },
202
- ...this.subDexes.map((dex) => ({
203
- subKey: `allMids:${dex}`,
204
- payload: { type: 'allMids', dex },
205
- })),
251
+ { subKey: 'allDexsAssetCtxs', payload: { type: 'allDexsAssetCtxs' } },
252
+ { subKey: 'fastAssetCtxs', payload: { type: 'fastAssetCtxs' } },
206
253
  ]
207
254
  }
208
255
 
256
+ private registerOrderbook(marketId: string, orderbookKey: string): void {
257
+ let activeKeys = this.orderbookKeysByMarketId.get(marketId)
258
+ if (activeKeys === undefined) {
259
+ activeKeys = new Set()
260
+ this.orderbookKeysByMarketId.set(marketId, activeKeys)
261
+ }
262
+
263
+ activeKeys.add(orderbookKey)
264
+ }
265
+
266
+ private releaseOrderbook(marketId: string, orderbookKey: string): void {
267
+ const activeKeys = this.orderbookKeysByMarketId.get(marketId)
268
+ if (activeKeys === undefined) {
269
+ return
270
+ }
271
+
272
+ activeKeys.delete(orderbookKey)
273
+ if (activeKeys.size > 0) {
274
+ return
275
+ }
276
+
277
+ this.orderbookKeysByMarketId.delete(marketId)
278
+ }
279
+
209
280
  protected override onClose(): void {
210
- this.midsBySubDex.clear()
281
+ this.perpCtxBySubDex.clear()
282
+ this.fastCtxByMarketId = {}
283
+ this.orderbookKeysByMarketId.clear()
284
+ this.latestOrderbookByMarketId.clear()
211
285
  this.orderUpdatesKey = undefined
212
286
  }
213
287
 
@@ -219,12 +293,17 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
219
293
 
220
294
  protected toKey(sub: Subscription): string {
221
295
  switch (sub.channel) {
222
- case 'prices':
223
- return 'allMids'
296
+ case 'marketsContext':
297
+ return 'marketsContext'
224
298
  case 'orderbook':
225
- return `l2Book:${sub.marketId}`
299
+ return `l2:${sub.marketId}:${this.orderbookAggregationKey(
300
+ sub.marketId,
301
+ sub.priceStep
302
+ )}`
226
303
  case 'candle':
227
304
  return `candle:${sub.marketId}:${sub.interval}`
305
+ case 'trades':
306
+ return `trades:${sub.marketId}`
228
307
  case 'orderUpdates':
229
308
  return `orderUpdates:${sub.address.toLowerCase()}`
230
309
  case 'fills':
@@ -236,32 +315,61 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
236
315
  }
237
316
  }
238
317
 
318
+ private orderbookAggregation(
319
+ marketId: string,
320
+ priceStep: number | undefined
321
+ ): { nSigFigs?: number; mantissa?: number } {
322
+ if (priceStep === undefined) {
323
+ return {}
324
+ }
325
+ return priceStepToAggregation(
326
+ priceStep,
327
+ this.orderbookReferencePrice(marketId)
328
+ )
329
+ }
330
+
331
+ private orderbookAggregationKey(
332
+ marketId: string,
333
+ priceStep: number | undefined
334
+ ): string {
335
+ const aggregation = this.orderbookAggregation(marketId, priceStep)
336
+ const nSigFigs = aggregation.nSigFigs ?? null
337
+ const mantissa = aggregation.mantissa ?? null
338
+ if (nSigFigs === null && mantissa === null) {
339
+ return 'full'
340
+ }
341
+ return `s:${nSigFigs}:m:${mantissa}`
342
+ }
343
+
239
344
  private toHlPayload(sub: Subscription): object {
240
345
  switch (sub.channel) {
241
- case 'prices':
242
- // Prices are handled via getPriceSubEntries in subscribe()
243
- // and never reach toHlPayload, but TS requires exhaustive cases.
244
- return { type: 'allMids' }
245
- case 'orderbook':
346
+ case 'marketsContext':
347
+ // Handled via getMarketsContextSubEntries in openChannel; never reaches
348
+ // toHlPayload, but TS requires an exhaustive switch.
349
+ return { type: 'allDexsAssetCtxs' }
350
+ case 'orderbook': {
351
+ const aggregation = this.orderbookAggregation(
352
+ sub.marketId,
353
+ sub.priceStep
354
+ )
246
355
  return {
247
- type: 'l2Book',
248
- coin: sub.marketId,
249
- // HL's l2Book ignores any level-count field; it returns up to 20
250
- // levels/side and controls granularity via nSigFigs (+ mantissa,
251
- // valid only when nSigFigs === 5).
252
- ...(sub.priceStep !== undefined
253
- ? priceStepToAggregation(
254
- sub.priceStep,
255
- this.mergedMids().get(sub.marketId) ?? Number.NaN
256
- )
257
- : {}),
356
+ type: 'l2',
357
+ c: sub.marketId,
358
+ s: aggregation.nSigFigs ?? null,
359
+ m: aggregation.mantissa ?? null,
258
360
  }
361
+ }
259
362
  case 'candle':
260
363
  return {
261
364
  type: 'candle',
262
365
  coin: sub.marketId,
263
366
  interval: sub.interval,
264
367
  }
368
+ case 'trades':
369
+ return {
370
+ type: 'trades',
371
+ coin: sub.marketId,
372
+ }
265
373
  case 'orderUpdates':
266
374
  return { type: 'orderUpdates', user: sub.address }
267
375
  case 'fills':
@@ -301,8 +409,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
301
409
 
302
410
  try {
303
411
  switch (msg.channel) {
304
- case 'allMids':
305
- this.handleAllMids(msg.data as HlWsAllMidsData)
412
+ case 'allDexsAssetCtxs':
413
+ this.handleAllDexsAssetCtxs(msg.data as HlWsAllDexsAssetCtxsData)
414
+ break
415
+ case 'fastAssetCtxs':
416
+ this.handleFastAssetCtxs(msg.data as string)
417
+ break
418
+ case 'l2':
419
+ this.handleL2(msg.data as HlWsL2Data)
306
420
  break
307
421
  case 'l2Book':
308
422
  this.handleL2Book(msg.data as HlWsL2BookData)
@@ -310,6 +424,9 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
310
424
  case 'candle':
311
425
  this.handleCandle(msg.data as HlWsCandleData)
312
426
  break
427
+ case 'trades':
428
+ this.handleTrades(msg.data as HlWsTrade[])
429
+ break
313
430
  case 'orderUpdates':
314
431
  this.handleOrderUpdates(msg.data as HlOrderDetail[])
315
432
  break
@@ -330,44 +447,179 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
330
447
  }
331
448
  }
332
449
 
333
- private handleAllMids(data: HlWsAllMidsData) {
334
- const subDexKey = data.dex || 'default'
335
- this.midsBySubDex.set(subDexKey, data.mids)
336
-
337
- const merged: Record<string, string> = {}
338
- for (const mids of this.midsBySubDex.values()) {
339
- Object.assign(merged, mids)
450
+ private handleAllDexsAssetCtxs(data: HlWsAllDexsAssetCtxsData) {
451
+ for (const [dex, ctxs] of data.assetCtxs) {
452
+ const byMarketId: Record<string, HlWsPerpAssetCtx> = {}
453
+ for (const ctx of ctxs) {
454
+ byMarketId[ctx.coin] = ctx
455
+ }
456
+ this.perpCtxBySubDex.set(dex || 'default', byMarketId)
340
457
  }
458
+ this.emitMarketsContext()
459
+ }
460
+
461
+ /**
462
+ * Decode a `fastAssetCtxs` frame (base64 + raw-DEFLATE) and merge its changed
463
+ * coins into the per-market fast-context store. Frames are incremental, so a
464
+ * field absent from this frame keeps its prior value. Decodes are chained to
465
+ * preserve arrival order across the async boundary.
466
+ */
467
+ private handleFastAssetCtxs(base64: string) {
468
+ this.fastDecodeChain = this.fastDecodeChain
469
+ .then(async () => {
470
+ const ctxs = await decodeFastAssetCtxs(base64)
471
+ for (const [marketId, ctx] of Object.entries(ctxs)) {
472
+ const prev = this.fastCtxByMarketId[marketId]
473
+ this.fastCtxByMarketId[marketId] = {
474
+ markPx: 'markPx' in ctx ? ctx.markPx : prev?.markPx,
475
+ midPx: 'midPx' in ctx ? ctx.midPx : prev?.midPx,
476
+ }
477
+ }
478
+ this.emitMarketsContext()
479
+ })
480
+ .catch((error) => wsLog.handlerFailure(this.providerKey, error))
481
+ }
341
482
 
342
- this.emit('allMids', { channel: 'prices', data: merged })
483
+ /**
484
+ * Build the all-markets context map: every perp asset context across sub-dexs
485
+ * (oracle/funding/OI/metadata), with mid + mark overlaid from the
486
+ * high-frequency `fastAssetCtxs` feed. A market present in `fastAssetCtxs` but
487
+ * without an asset-context frame yet (the aggregate feed is slow/idle) still
488
+ * gets a mid+mark entry, so price ticks without waiting on the aggregate.
489
+ */
490
+ private emitMarketsContext() {
491
+ const data: Record<string, MarketContext> = {}
492
+ for (const byMarketId of this.perpCtxBySubDex.values()) {
493
+ for (const [marketId, ctx] of Object.entries(byMarketId)) {
494
+ const base = mapMarketContext(marketId, ctx)
495
+ const fast = this.fastCtxByMarketId[marketId]
496
+ data[marketId] = {
497
+ ...base,
498
+ midPrice: fast?.midPx != null ? fast.midPx : base.midPrice,
499
+ markPrice: fast?.markPx != null ? fast.markPx : base.markPrice,
500
+ }
501
+ }
502
+ }
503
+ for (const [marketId, fast] of Object.entries(this.fastCtxByMarketId)) {
504
+ if (data[marketId] !== undefined) {
505
+ continue
506
+ }
507
+ // No asset-context frame for this market yet — each price stands in for
508
+ // the other where the fast feed carries only one of mid/mark.
509
+ const midPrice = fast.midPx ?? fast.markPx
510
+ const markPrice = fast.markPx ?? fast.midPx
511
+ if (midPrice != null && markPrice != null) {
512
+ data[marketId] = { marketId, midPrice, markPrice }
513
+ }
514
+ }
515
+ this.emit('marketsContext', { channel: 'marketsContext', data })
343
516
  }
344
517
 
345
- /** Latest mid per `Market.id`, merged across sub-dex allMids frames. */
518
+ /** Latest mid per `Market.id` across the perp ctx feed and `fastAssetCtxs`. */
346
519
  private mergedMids(): Map<string, number> {
347
520
  const map = new Map<string, number>()
348
- for (const mids of this.midsBySubDex.values()) {
349
- for (const [id, mid] of Object.entries(mids)) {
521
+ for (const byMarketId of this.perpCtxBySubDex.values()) {
522
+ for (const [id, ctx] of Object.entries(byMarketId)) {
523
+ const mid = ctx.midPx ?? ctx.markPx
524
+ map.set(id, Number(mid))
525
+ }
526
+ }
527
+ for (const [id, fast] of Object.entries(this.fastCtxByMarketId)) {
528
+ const mid = fast.midPx ?? fast.markPx
529
+ if (mid != null) {
350
530
  map.set(id, Number(mid))
351
531
  }
352
532
  }
353
533
  return map
354
534
  }
355
535
 
536
+ private orderbookReferencePrice(marketId: string): number {
537
+ const mid = this.mergedMids().get(marketId)
538
+ if (mid !== undefined) {
539
+ return mid
540
+ }
541
+
542
+ const book = this.latestOrderbookByMarketId.get(marketId)
543
+ const bid = book?.bids[0]?.price
544
+ const ask = book?.asks[0]?.price
545
+ if (bid !== undefined && ask !== undefined) {
546
+ return (Number(bid) + Number(ask)) / 2
547
+ }
548
+
549
+ return Number.NaN
550
+ }
551
+
356
552
  private handleL2Book(data: HlWsL2BookData) {
357
- this.emit(`l2Book:${data.coin}`, {
358
- channel: 'orderbook',
359
- data: {
360
- provider: this.providerKey,
361
- marketId: data.coin,
362
- bids: data.levels[0]
363
- .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
364
- .map((l) => ({ price: l.px, size: l.sz })),
365
- asks: data.levels[1]
366
- .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
367
- .map((l) => ({ price: l.px, size: l.sz })),
368
- timestamp: data.time,
369
- },
370
- })
553
+ const book = {
554
+ provider: this.providerKey,
555
+ marketId: data.coin,
556
+ bids: data.levels[0]
557
+ .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
558
+ .map((l) => ({ price: l.px, size: l.sz })),
559
+ asks: data.levels[1]
560
+ .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
561
+ .map((l) => ({ price: l.px, size: l.sz })),
562
+ timestamp: data.time,
563
+ }
564
+ this.latestOrderbookByMarketId.set(data.coin, book)
565
+ this.emitOrderbook(data.coin, book)
566
+ }
567
+
568
+ private handleL2(data: HlWsL2Data) {
569
+ if (data.s !== undefined) {
570
+ this.handleL2Book(data.s)
571
+ }
572
+ if (data.u !== undefined) {
573
+ this.handleCompactL2Delta(data.u)
574
+ }
575
+ if (data.c === undefined) {
576
+ return
577
+ }
578
+ const compressed = data.c
579
+ this.orderbookDecodeChain = this.orderbookDecodeChain
580
+ .then(async () => {
581
+ this.handleCompactL2Delta(
582
+ await decodeCompressedJson<HlWsCompressedL2Data>(compressed)
583
+ )
584
+ })
585
+ .catch((error) => wsLog.handlerFailure(this.providerKey, error))
586
+ }
587
+
588
+ private handleCompactL2Delta(delta: HlWsCompressedL2Data) {
589
+ const previous = this.latestOrderbookByMarketId.get(delta.c)
590
+ if (previous === undefined) {
591
+ return
592
+ }
593
+
594
+ const book: OrderbookResponse = {
595
+ provider: this.providerKey,
596
+ marketId: delta.c,
597
+ bids: applyCompressedL2Side(
598
+ previous.bids,
599
+ delta.l[0],
600
+ delta.r?.[0] ?? [],
601
+ 'bid'
602
+ ),
603
+ asks: applyCompressedL2Side(
604
+ previous.asks,
605
+ delta.l[1],
606
+ delta.r?.[1] ?? [],
607
+ 'ask'
608
+ ),
609
+ timestamp: delta.t,
610
+ }
611
+ this.latestOrderbookByMarketId.set(delta.c, book)
612
+ this.emitOrderbook(delta.c, book)
613
+ }
614
+
615
+ private emitOrderbook(marketId: string, book: OrderbookResponse) {
616
+ const keys = this.orderbookKeysByMarketId.get(marketId)
617
+ if (keys === undefined) {
618
+ return
619
+ }
620
+ for (const key of keys) {
621
+ this.emit(key, { channel: 'orderbook', data: book })
622
+ }
371
623
  }
372
624
 
373
625
  private handleCandle(data: HlWsCandleData) {
@@ -384,6 +636,25 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
384
636
  })
385
637
  }
386
638
 
639
+ private handleTrades(data: HlWsTrade[]) {
640
+ for (const trade of data) {
641
+ this.emit(`trades:${trade.coin}`, {
642
+ channel: 'trades',
643
+ data: [
644
+ {
645
+ provider: this.providerKey,
646
+ marketId: trade.coin,
647
+ price: trade.px,
648
+ size: trade.sz,
649
+ timestamp: trade.time,
650
+ side: trade.side === 'B' ? 'buy' : 'sell',
651
+ id: trade.tid !== undefined ? String(trade.tid) : trade.hash,
652
+ },
653
+ ],
654
+ })
655
+ }
656
+ }
657
+
387
658
  private handleOrderUpdates(data: HlOrderDetail[]) {
388
659
  // Untagged frame: attributable only via the single-address invariant.
389
660
  const key = this.orderUpdatesKey
@@ -489,6 +760,47 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
489
760
  const isObject = (v: unknown): v is Record<string, unknown> =>
490
761
  typeof v === 'object' && v !== null
491
762
 
763
+ function compactRemovalPrice(
764
+ levels: OrderbookLevel[],
765
+ value: number | string | { p: string }
766
+ ): string | undefined {
767
+ if (typeof value === 'number') {
768
+ return levels[value]?.price
769
+ }
770
+ return typeof value === 'string' ? value : value.p
771
+ }
772
+
773
+ function applyCompressedL2Side(
774
+ previous: OrderbookLevel[],
775
+ updates: HlWsCompressedL2Data['l'][number],
776
+ removals: NonNullable<HlWsCompressedL2Data['r']>[number],
777
+ side: 'bid' | 'ask'
778
+ ): OrderbookLevel[] {
779
+ const byPrice = new Map(previous.map((level) => [level.price, level.size]))
780
+ for (const removal of removals) {
781
+ const price = compactRemovalPrice(previous, removal)
782
+ if (price !== undefined) {
783
+ byPrice.delete(price)
784
+ }
785
+ }
786
+ for (const update of updates) {
787
+ if (Number(update.s) === 0) {
788
+ byPrice.delete(update.p)
789
+ } else {
790
+ byPrice.set(update.p, update.s)
791
+ }
792
+ }
793
+
794
+ return [...byPrice.entries()]
795
+ .map(([price, size]) => ({ price, size }))
796
+ .sort((a, b) =>
797
+ side === 'bid'
798
+ ? Number(b.price) - Number(a.price)
799
+ : Number(a.price) - Number(b.price)
800
+ )
801
+ .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
802
+ }
803
+
492
804
  /**
493
805
  * Minimal presence/type check of the channel-discriminating and required
494
806
  * fields the matching handler dereferences without its own guard. A frame
@@ -497,12 +809,16 @@ const isObject = (v: unknown): v is Record<string, unknown> =>
497
809
  * to the dispatch switch, which silently ignores them.
498
810
  */
499
811
  function isValidHlFrame(channel: string, data: unknown): boolean {
812
+ // `fastAssetCtxs` carries a base64 string payload, not an object.
813
+ if (channel === 'fastAssetCtxs') {
814
+ return typeof data === 'string'
815
+ }
500
816
  if (!isObject(data)) {
501
817
  return false
502
818
  }
503
819
  switch (channel) {
504
- case 'allMids':
505
- return isObject(data.mids)
820
+ case 'allDexsAssetCtxs':
821
+ return Array.isArray(data.assetCtxs)
506
822
  case 'l2Book':
507
823
  return (
508
824
  typeof data.coin === 'string' &&
@@ -511,8 +827,12 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
511
827
  Array.isArray(data.levels[1]) &&
512
828
  typeof data.time === 'number'
513
829
  )
830
+ case 'l2':
831
+ return typeof data.c === 'string' || isObject(data.s) || isObject(data.u)
514
832
  case 'candle':
515
833
  return typeof data.s === 'string' && typeof data.i === 'string'
834
+ case 'trades':
835
+ return Array.isArray(data)
516
836
  case 'orderUpdates':
517
837
  return Array.isArray(data)
518
838
  case 'userFills':
@@ -1,4 +0,0 @@
1
- import type { MarketPrice } from '@lifi/perps-types';
2
- import type { HlAssetCtx } from '../types/index.js';
3
- export declare const mapMarketPrice: (marketId: string, assetCtx: HlAssetCtx, mid: string) => MarketPrice;
4
- //# sourceMappingURL=mapMarketPrice.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapMarketPrice.d.ts","sourceRoot":"","sources":["../../../src/utils/mapMarketPrice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAUnD,eAAO,MAAM,cAAc,GACzB,UAAU,MAAM,EAChB,UAAU,UAAU,EACpB,KAAK,MAAM,KACV,WAiBF,CAAA"}