@lifi/perps-sdk-provider-hyperliquid 1.1.0 → 1.3.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 (41) hide show
  1. package/dist/cjs/types/ws.d.ts +48 -1
  2. package/dist/cjs/types/ws.d.ts.map +1 -1
  3. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts +1 -0
  4. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  5. package/dist/cjs/utils/decodeFastAssetCtxs.js +5 -1
  6. package/dist/cjs/utils/decodeFastAssetCtxs.js.map +1 -1
  7. package/dist/cjs/utils/index.d.ts +1 -1
  8. package/dist/cjs/utils/index.d.ts.map +1 -1
  9. package/dist/cjs/utils/index.js +2 -1
  10. package/dist/cjs/utils/index.js.map +1 -1
  11. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +20 -0
  12. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  13. package/dist/cjs/websocket/HyperliquidWsProvider.js +359 -29
  14. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  15. package/dist/esm/types/ws.d.ts +65 -1
  16. package/dist/esm/types/ws.d.ts.map +1 -1
  17. package/dist/esm/utils/decodeFastAssetCtxs.d.ts +1 -0
  18. package/dist/esm/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  19. package/dist/esm/utils/decodeFastAssetCtxs.js +9 -6
  20. package/dist/esm/utils/decodeFastAssetCtxs.js.map +1 -1
  21. package/dist/esm/utils/index.d.ts +1 -1
  22. package/dist/esm/utils/index.d.ts.map +1 -1
  23. package/dist/esm/utils/index.js +1 -1
  24. package/dist/esm/utils/index.js.map +1 -1
  25. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +25 -8
  26. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  27. package/dist/esm/websocket/HyperliquidWsProvider.js +368 -48
  28. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  29. package/dist/types/types/ws.d.ts +65 -1
  30. package/dist/types/types/ws.d.ts.map +1 -1
  31. package/dist/types/utils/decodeFastAssetCtxs.d.ts +1 -0
  32. package/dist/types/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  33. package/dist/types/utils/index.d.ts +1 -1
  34. package/dist/types/utils/index.d.ts.map +1 -1
  35. package/dist/types/websocket/HyperliquidWsProvider.d.ts +25 -8
  36. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  37. package/package.json +3 -3
  38. package/src/types/ws.ts +67 -1
  39. package/src/utils/decodeFastAssetCtxs.ts +10 -6
  40. package/src/utils/index.ts +4 -1
  41. package/src/websocket/HyperliquidWsProvider.ts +464 -50
@@ -14,12 +14,15 @@ import {
14
14
  import {
15
15
  type MarketContext,
16
16
  type OpenOrder,
17
+ type OrderbookLevel,
18
+ type OrderbookResponse,
17
19
  OrderSide,
18
20
  OrderType,
19
21
  type Subscription,
20
22
  type TriggerOrder,
21
23
  } from '@lifi/perps-types'
22
- import { HYPERLIQUID_FEE_TIER_FALLBACK } from '../constants.js'
24
+ import Big from 'big.js'
25
+ import { HYPERLIQUID_FEE_TIER_FALLBACK, SPOT_MARKET_ID } from '../constants.js'
23
26
  import type {
24
27
  HlAssetPosition,
25
28
  HlOrderDetail,
@@ -27,14 +30,22 @@ import type {
27
30
  HlWsAllDexsAssetCtxsData,
28
31
  HlWsAllDexsClearinghouseStateData,
29
32
  HlWsCandleData,
33
+ HlWsCompressedL2Data,
30
34
  HlWsFastAssetCtx,
31
35
  HlWsL2BookData,
36
+ HlWsL2Data,
32
37
  HlWsMessage,
38
+ HlWsPacData,
33
39
  HlWsPerpAssetCtx,
40
+ HlWsPerpAssetCtxPayload,
41
+ HlWsSacData,
42
+ HlWsSpotAssetCtx,
34
43
  HlWsSpotStateData,
44
+ HlWsTrade,
35
45
  HlWsUserFillsData,
36
46
  } from '../types/index.js'
37
47
  import {
48
+ decodeCompressedJson,
38
49
  decodeFastAssetCtxs,
39
50
  isOpenAssetPosition,
40
51
  isTriggerOrder,
@@ -49,7 +60,7 @@ import {
49
60
  spotPriceById,
50
61
  } from '../utils/index.js'
51
62
 
52
- /** HL's `l2Book` returns at most this many levels per side (slow/default mode). */
63
+ /** HL's compact `l2` snapshot carries 20 levels per side. */
53
64
  const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20
54
65
 
55
66
  /**
@@ -85,14 +96,20 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
85
96
  private readonly client: PerpsSDKClient | undefined
86
97
  private readonly registry: MarketRegistry | undefined
87
98
  private perpCtxBySubDex = new Map<string, Record<string, HlWsPerpAssetCtx>>()
99
+ private spotCtxByMarketId: Record<string, HlWsSpotAssetCtx> = {}
100
+ private orderbookKeysByMarketId = new Map<string, Set<string>>()
101
+ private latestOrderbookByMarketId = new Map<string, OrderbookResponse>()
88
102
  // Latest mid/mark per `Market.id` from the high-frequency `fastAssetCtxs`
89
103
  // feed (all dexes, incl. builder/sub-dex coins). Merged incrementally —
90
- // frames carry only changed coins — and overlaid onto the slower
91
- // `allDexsAssetCtxs` context (which supplies oracle/funding/OI/metadata).
104
+ // frames carry only changed coins — and overlaid onto asset contexts that
105
+ // carry oracle/funding/OI/metadata.
92
106
  private fastCtxByMarketId: Record<string, HlWsFastAssetCtx> = {}
93
107
  // `fastAssetCtxs` payloads are base64 + raw-DEFLATE; decode is async, so
94
108
  // chain decodes to apply incremental frames in arrival order.
95
109
  private fastDecodeChain: Promise<void> = Promise.resolve()
110
+ private pacDecodeChain: Promise<void> = Promise.resolve()
111
+ private sacDecodeChain: Promise<void> = Promise.resolve()
112
+ private orderbookDecodeChain: Promise<void> = Promise.resolve()
96
113
 
97
114
  constructor(wsUrl: string, providerKey: string, client?: PerpsSDKClient) {
98
115
  super(
@@ -133,9 +150,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
133
150
 
134
151
  await this.registry?.sync()
135
152
 
136
- // Markets context aggregates the all-dexs perp asset-context feed
137
- // (oracle, funding, OI, metadata) with the high-frequency `fastAssetCtxs`
138
- // feed, which supplies fast mid + mark for every market across all dexes.
153
+ // Markets context aggregates slower asset-context feeds with fast mid/mark
154
+ // ticks from `fastAssetCtxs`.
139
155
  if (sub.channel === 'marketsContext') {
140
156
  const entries = this.getMarketsContextSubEntries()
141
157
 
@@ -153,14 +169,21 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
153
169
  )
154
170
  }
155
171
  this.perpCtxBySubDex.clear()
172
+ this.spotCtxByMarketId = {}
156
173
  this.fastCtxByMarketId = {}
157
174
  }
158
175
  }
159
176
 
160
177
  // All other channels: single WS subscription per key
161
178
  const key = this.toKey(sub)
179
+ if (sub.channel === 'orderbook') {
180
+ this.claimOrderbookKey(sub.marketId, key)
181
+ }
162
182
  const payload = this.toHlPayload(sub)
163
183
  await this.registerSub(key, payload)
184
+ if (sub.channel === 'orderbook') {
185
+ this.registerOrderbook(sub.marketId, key)
186
+ }
164
187
 
165
188
  await this.rws.ready()
166
189
 
@@ -170,8 +193,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
170
193
  this.orderUpdatesKey = undefined
171
194
  }
172
195
  this.rws.send(
173
- JSON.stringify({ method: 'unsubscribe', subscription: payload })
196
+ JSON.stringify({
197
+ method: 'unsubscribe',
198
+ subscription: payload,
199
+ })
174
200
  )
201
+ if (sub.channel === 'orderbook') {
202
+ this.releaseOrderbook(sub.marketId, key)
203
+ }
175
204
  }
176
205
  }
177
206
 
@@ -198,9 +227,28 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
198
227
  this.orderUpdatesKey = key
199
228
  }
200
229
 
230
+ private claimOrderbookKey(marketId: string, key: string): void {
231
+ const activeKeys = this.orderbookKeysByMarketId.get(marketId)
232
+ if (activeKeys === undefined) {
233
+ return
234
+ }
235
+
236
+ for (const activeKey of [...activeKeys]) {
237
+ if (activeKey === key) {
238
+ continue
239
+ }
240
+ if (!this.closeChannelIfIdle(activeKey)) {
241
+ throw new Error(
242
+ `Hyperliquid supports one orderbook aggregation per market per ` +
243
+ `provider instance. Unsubscribe ${activeKey} before subscribing ${key}.`
244
+ )
245
+ }
246
+ }
247
+ }
248
+
201
249
  /**
202
- * Sub-key + payload pairs for the markets-context aggregation: the all-dexs
203
- * perp asset-context feed (oracle/funding/OI/metadata) plus the compressed
250
+ * Sub-key + payload pairs for the markets-context aggregation: compressed
251
+ * all-perp (`pac`) and all-spot (`sac`) asset contexts plus the documented
204
252
  * `fastAssetCtxs` feed (high-frequency mid + mark across every dex).
205
253
  */
206
254
  private getMarketsContextSubEntries(): Array<{
@@ -208,14 +256,46 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
208
256
  payload: object
209
257
  }> {
210
258
  return [
211
- { subKey: 'allDexsAssetCtxs', payload: { type: 'allDexsAssetCtxs' } },
259
+ { subKey: 'pac', payload: { type: 'pac' } },
260
+ { subKey: 'sac', payload: { type: 'sac' } },
212
261
  { subKey: 'fastAssetCtxs', payload: { type: 'fastAssetCtxs' } },
213
262
  ]
214
263
  }
215
264
 
265
+ private registerOrderbook(marketId: string, orderbookKey: string): void {
266
+ let activeKeys = this.orderbookKeysByMarketId.get(marketId)
267
+ if (activeKeys === undefined) {
268
+ activeKeys = new Set()
269
+ this.orderbookKeysByMarketId.set(marketId, activeKeys)
270
+ }
271
+
272
+ activeKeys.add(orderbookKey)
273
+ }
274
+
275
+ private releaseOrderbook(marketId: string, orderbookKey: string): void {
276
+ const activeKeys = this.orderbookKeysByMarketId.get(marketId)
277
+ if (activeKeys === undefined) {
278
+ return
279
+ }
280
+
281
+ activeKeys.delete(orderbookKey)
282
+ if (activeKeys.size > 0) {
283
+ return
284
+ }
285
+
286
+ this.orderbookKeysByMarketId.delete(marketId)
287
+ }
288
+
216
289
  protected override onClose(): void {
217
290
  this.perpCtxBySubDex.clear()
291
+ this.spotCtxByMarketId = {}
218
292
  this.fastCtxByMarketId = {}
293
+ this.orderbookKeysByMarketId.clear()
294
+ this.latestOrderbookByMarketId.clear()
295
+ this.fastDecodeChain = Promise.resolve()
296
+ this.pacDecodeChain = Promise.resolve()
297
+ this.sacDecodeChain = Promise.resolve()
298
+ this.orderbookDecodeChain = Promise.resolve()
219
299
  this.orderUpdatesKey = undefined
220
300
  }
221
301
 
@@ -230,9 +310,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
230
310
  case 'marketsContext':
231
311
  return 'marketsContext'
232
312
  case 'orderbook':
233
- return `l2Book:${sub.marketId}`
313
+ return `l2:${sub.marketId}:${this.orderbookAggregationKey(
314
+ sub.marketId,
315
+ sub.priceStep
316
+ )}`
234
317
  case 'candle':
235
318
  return `candle:${sub.marketId}:${sub.interval}`
319
+ case 'trades':
320
+ return `trades:${sub.marketId}`
236
321
  case 'orderUpdates':
237
322
  return `orderUpdates:${sub.address.toLowerCase()}`
238
323
  case 'fills':
@@ -244,32 +329,61 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
244
329
  }
245
330
  }
246
331
 
332
+ private orderbookAggregation(
333
+ marketId: string,
334
+ priceStep: number | undefined
335
+ ): { nSigFigs?: number; mantissa?: number } {
336
+ if (priceStep === undefined) {
337
+ return {}
338
+ }
339
+ return priceStepToAggregation(
340
+ priceStep,
341
+ this.orderbookReferencePrice(marketId)
342
+ )
343
+ }
344
+
345
+ private orderbookAggregationKey(
346
+ marketId: string,
347
+ priceStep: number | undefined
348
+ ): string {
349
+ const aggregation = this.orderbookAggregation(marketId, priceStep)
350
+ const nSigFigs = aggregation.nSigFigs ?? null
351
+ const mantissa = aggregation.mantissa ?? null
352
+ if (nSigFigs === null && mantissa === null) {
353
+ return 'full'
354
+ }
355
+ return `s:${nSigFigs}:m:${mantissa}`
356
+ }
357
+
247
358
  private toHlPayload(sub: Subscription): object {
248
359
  switch (sub.channel) {
249
360
  case 'marketsContext':
250
361
  // Handled via getMarketsContextSubEntries in openChannel; never reaches
251
362
  // toHlPayload, but TS requires an exhaustive switch.
252
363
  return { type: 'allDexsAssetCtxs' }
253
- case 'orderbook':
364
+ case 'orderbook': {
365
+ const aggregation = this.orderbookAggregation(
366
+ sub.marketId,
367
+ sub.priceStep
368
+ )
254
369
  return {
255
- type: 'l2Book',
256
- coin: sub.marketId,
257
- // HL's l2Book ignores any level-count field; it returns up to 20
258
- // levels/side and controls granularity via nSigFigs (+ mantissa,
259
- // valid only when nSigFigs === 5).
260
- ...(sub.priceStep !== undefined
261
- ? priceStepToAggregation(
262
- sub.priceStep,
263
- this.mergedMids().get(sub.marketId) ?? Number.NaN
264
- )
265
- : {}),
370
+ type: 'l2',
371
+ c: sub.marketId,
372
+ s: aggregation.nSigFigs ?? null,
373
+ m: aggregation.mantissa ?? null,
266
374
  }
375
+ }
267
376
  case 'candle':
268
377
  return {
269
378
  type: 'candle',
270
379
  coin: sub.marketId,
271
380
  interval: sub.interval,
272
381
  }
382
+ case 'trades':
383
+ return {
384
+ type: 'trades',
385
+ coin: sub.marketId,
386
+ }
273
387
  case 'orderUpdates':
274
388
  return { type: 'orderUpdates', user: sub.address }
275
389
  case 'fills':
@@ -312,15 +426,27 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
312
426
  case 'allDexsAssetCtxs':
313
427
  this.handleAllDexsAssetCtxs(msg.data as HlWsAllDexsAssetCtxsData)
314
428
  break
429
+ case 'pac':
430
+ this.handlePac(msg.data as string)
431
+ break
432
+ case 'sac':
433
+ this.handleSac(msg.data as string)
434
+ break
315
435
  case 'fastAssetCtxs':
316
436
  this.handleFastAssetCtxs(msg.data as string)
317
437
  break
438
+ case 'l2':
439
+ this.handleL2(msg.data as HlWsL2Data)
440
+ break
318
441
  case 'l2Book':
319
442
  this.handleL2Book(msg.data as HlWsL2BookData)
320
443
  break
321
444
  case 'candle':
322
445
  this.handleCandle(msg.data as HlWsCandleData)
323
446
  break
447
+ case 'trades':
448
+ this.handleTrades(msg.data as HlWsTrade[])
449
+ break
324
450
  case 'orderUpdates':
325
451
  this.handleOrderUpdates(msg.data as HlOrderDetail[])
326
452
  break
@@ -342,14 +468,76 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
342
468
  }
343
469
 
344
470
  private handleAllDexsAssetCtxs(data: HlWsAllDexsAssetCtxsData) {
345
- for (const [dex, ctxs] of data.assetCtxs) {
346
- const byMarketId: Record<string, HlWsPerpAssetCtx> = {}
471
+ this.mergePerpAssetCtxEntries(allDexsAssetCtxEntries(data))
472
+ this.emitMarketsContext()
473
+ }
474
+
475
+ private handlePac(base64: string) {
476
+ this.pacDecodeChain = this.pacDecodeChain
477
+ .then(async () => {
478
+ this.mergePerpAssetCtxEntries(
479
+ await decodeCompressedJson<HlWsPacData>(base64)
480
+ )
481
+ this.emitMarketsContext()
482
+ })
483
+ .catch((error) => wsLog.handlerFailure(this.providerKey, error))
484
+ }
485
+
486
+ private mergePerpAssetCtxEntries(
487
+ entries: [string, HlWsPerpAssetCtxPayload[]][]
488
+ ) {
489
+ for (const [dex, ctxs] of entries) {
490
+ const previous = this.perpCtxBySubDex.get(dex || 'default')
491
+ const byMarketId: Record<string, HlWsPerpAssetCtx> = {
492
+ ...(previous ?? {}),
493
+ }
347
494
  for (const ctx of ctxs) {
348
- byMarketId[ctx.coin] = ctx
495
+ const marketId = ctx.coin
496
+ if (marketId === undefined) {
497
+ continue
498
+ }
499
+ const merged = mergePerpAssetCtx(previous?.[marketId], marketId, ctx)
500
+ if (merged !== undefined) {
501
+ byMarketId[marketId] = merged
502
+ }
349
503
  }
350
504
  this.perpCtxBySubDex.set(dex || 'default', byMarketId)
351
505
  }
352
- this.emitMarketsContext()
506
+ }
507
+
508
+ private handleSac(base64: string) {
509
+ this.sacDecodeChain = this.sacDecodeChain
510
+ .then(async () => {
511
+ const spotMarketIdBySacKey = this.spotMarketIdBySacKey()
512
+ const ctxs = await decodeCompressedJson<HlWsSacData>(base64)
513
+ for (const [sacKey, ctx] of Object.entries(ctxs)) {
514
+ const marketId = spotMarketIdBySacKey.get(sacKey)
515
+ if (marketId === undefined) {
516
+ continue
517
+ }
518
+ this.spotCtxByMarketId[marketId] = {
519
+ ...this.spotCtxByMarketId[marketId],
520
+ ...ctx,
521
+ }
522
+ }
523
+ this.emitMarketsContext()
524
+ })
525
+ .catch((error) => wsLog.handlerFailure(this.providerKey, error))
526
+ }
527
+
528
+ private spotMarketIdBySacKey(): Map<string, string> {
529
+ const map = new Map<string, string>()
530
+ for (const market of this.registry?.markets ?? []) {
531
+ if (market.categoryId !== SPOT_MARKET_ID) {
532
+ continue
533
+ }
534
+ map.set(market.id, market.id)
535
+ map.set(
536
+ `${market.baseAsset.displaySymbol}/${market.quoteAsset.displaySymbol}`,
537
+ market.id
538
+ )
539
+ }
540
+ return map
353
541
  }
354
542
 
355
543
  /**
@@ -375,11 +563,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
375
563
  }
376
564
 
377
565
  /**
378
- * Build the all-markets context map: every perp asset context across sub-dexs
379
- * (oracle/funding/OI/metadata), with mid + mark overlaid from the
380
- * high-frequency `fastAssetCtxs` feed. A market present in `fastAssetCtxs` but
381
- * without an asset-context frame yet (the aggregate feed is slow/idle) still
382
- * gets a mid+mark entry, so price ticks without waiting on the aggregate.
566
+ * Build the all-markets context map from perp + spot asset contexts, with mid
567
+ * + mark overlaid from `fastAssetCtxs`.
383
568
  */
384
569
  private emitMarketsContext() {
385
570
  const data: Record<string, MarketContext> = {}
@@ -394,6 +579,23 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
394
579
  }
395
580
  }
396
581
  }
582
+ for (const [marketId, ctx] of Object.entries(this.spotCtxByMarketId)) {
583
+ const midPrice = toMarketContextString(ctx.midPx ?? ctx.markPx)
584
+ const markPrice = toMarketContextString(ctx.markPx)
585
+ if (midPrice === undefined || markPrice === undefined) {
586
+ continue
587
+ }
588
+ const fast = this.fastCtxByMarketId[marketId]
589
+ const emittedMarkPrice = fast?.markPx != null ? fast.markPx : markPrice
590
+ data[marketId] = {
591
+ marketId,
592
+ midPrice: fast?.midPx != null ? fast.midPx : midPrice,
593
+ markPrice: emittedMarkPrice,
594
+ prevDayPrice: toMarketContextString(ctx.prevDayPx),
595
+ volume24h: toMarketContextString(ctx.dayNtlVlm),
596
+ marketCap: toMarketCapString(emittedMarkPrice, ctx.circulatingSupply),
597
+ }
598
+ }
397
599
  for (const [marketId, fast] of Object.entries(this.fastCtxByMarketId)) {
398
600
  if (data[marketId] !== undefined) {
399
601
  continue
@@ -409,7 +611,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
409
611
  this.emit('marketsContext', { channel: 'marketsContext', data })
410
612
  }
411
613
 
412
- /** Latest mid per `Market.id` across the perp ctx feed and `fastAssetCtxs`. */
614
+ /** Latest mid per `Market.id` across asset-context and fast feeds. */
413
615
  private mergedMids(): Map<string, number> {
414
616
  const map = new Map<string, number>()
415
617
  for (const byMarketId of this.perpCtxBySubDex.values()) {
@@ -418,6 +620,12 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
418
620
  map.set(id, Number(mid))
419
621
  }
420
622
  }
623
+ for (const [id, ctx] of Object.entries(this.spotCtxByMarketId)) {
624
+ const mid = ctx.midPx ?? ctx.markPx
625
+ if (mid != null) {
626
+ map.set(id, Number(mid))
627
+ }
628
+ }
421
629
  for (const [id, fast] of Object.entries(this.fastCtxByMarketId)) {
422
630
  const mid = fast.midPx ?? fast.markPx
423
631
  if (mid != null) {
@@ -427,21 +635,93 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
427
635
  return map
428
636
  }
429
637
 
638
+ private orderbookReferencePrice(marketId: string): number {
639
+ const mid = this.mergedMids().get(marketId)
640
+ if (mid !== undefined) {
641
+ return mid
642
+ }
643
+
644
+ const book = this.latestOrderbookByMarketId.get(marketId)
645
+ const bid = book?.bids[0]?.price
646
+ const ask = book?.asks[0]?.price
647
+ if (bid !== undefined && ask !== undefined) {
648
+ return (Number(bid) + Number(ask)) / 2
649
+ }
650
+
651
+ return Number.NaN
652
+ }
653
+
430
654
  private handleL2Book(data: HlWsL2BookData) {
431
- this.emit(`l2Book:${data.coin}`, {
432
- channel: 'orderbook',
433
- data: {
434
- provider: this.providerKey,
435
- marketId: data.coin,
436
- bids: data.levels[0]
437
- .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
438
- .map((l) => ({ price: l.px, size: l.sz })),
439
- asks: data.levels[1]
440
- .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
441
- .map((l) => ({ price: l.px, size: l.sz })),
442
- timestamp: data.time,
443
- },
444
- })
655
+ const book = {
656
+ provider: this.providerKey,
657
+ marketId: data.coin,
658
+ bids: data.levels[0]
659
+ .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
660
+ .map((l) => ({ price: l.px, size: l.sz })),
661
+ asks: data.levels[1]
662
+ .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
663
+ .map((l) => ({ price: l.px, size: l.sz })),
664
+ timestamp: data.time,
665
+ }
666
+ this.latestOrderbookByMarketId.set(data.coin, book)
667
+ this.emitOrderbook(data.coin, book)
668
+ }
669
+
670
+ private handleL2(data: HlWsL2Data) {
671
+ if (data.s !== undefined) {
672
+ this.handleL2Book(data.s)
673
+ }
674
+ if (data.u !== undefined) {
675
+ this.handleCompactL2Delta(data.u)
676
+ }
677
+ if (data.c === undefined) {
678
+ return
679
+ }
680
+ const compressed = data.c
681
+ this.orderbookDecodeChain = this.orderbookDecodeChain
682
+ .then(async () => {
683
+ this.handleCompactL2Delta(
684
+ await decodeCompressedJson<HlWsCompressedL2Data>(compressed)
685
+ )
686
+ })
687
+ .catch((error) => wsLog.handlerFailure(this.providerKey, error))
688
+ }
689
+
690
+ private handleCompactL2Delta(delta: HlWsCompressedL2Data) {
691
+ const previous = this.latestOrderbookByMarketId.get(delta.c)
692
+ if (previous === undefined) {
693
+ return
694
+ }
695
+
696
+ const book: OrderbookResponse = {
697
+ provider: this.providerKey,
698
+ marketId: delta.c,
699
+ bids: applyCompressedL2Side(
700
+ previous.bids,
701
+ delta.l[0],
702
+ delta.r?.[0] ?? [],
703
+ 'bid'
704
+ ),
705
+ asks: applyCompressedL2Side(
706
+ previous.asks,
707
+ delta.l[1],
708
+ delta.r?.[1] ?? [],
709
+ 'ask'
710
+ ),
711
+ timestamp: delta.t,
712
+ }
713
+ this.latestOrderbookByMarketId.set(delta.c, book)
714
+ this.emitOrderbook(delta.c, book)
715
+ }
716
+
717
+ private emitOrderbook(marketId: string, book: OrderbookResponse) {
718
+ const keys = this.orderbookKeysByMarketId.get(marketId)
719
+ if (keys === undefined) {
720
+ return
721
+ }
722
+ for (const key of keys) {
723
+ this.emit(key, { channel: 'orderbook', data: book })
724
+ }
445
725
  }
446
726
 
447
727
  private handleCandle(data: HlWsCandleData) {
@@ -458,6 +738,25 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
458
738
  })
459
739
  }
460
740
 
741
+ private handleTrades(data: HlWsTrade[]) {
742
+ for (const trade of data) {
743
+ this.emit(`trades:${trade.coin}`, {
744
+ channel: 'trades',
745
+ data: [
746
+ {
747
+ provider: this.providerKey,
748
+ marketId: trade.coin,
749
+ price: trade.px,
750
+ size: trade.sz,
751
+ timestamp: trade.time,
752
+ side: trade.side === 'B' ? 'buy' : 'sell',
753
+ id: trade.tid !== undefined ? String(trade.tid) : trade.hash,
754
+ },
755
+ ],
756
+ })
757
+ }
758
+ }
759
+
461
760
  private handleOrderUpdates(data: HlOrderDetail[]) {
462
761
  // Untagged frame: attributable only via the single-address invariant.
463
762
  const key = this.orderUpdatesKey
@@ -563,6 +862,117 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
563
862
  const isObject = (v: unknown): v is Record<string, unknown> =>
564
863
  typeof v === 'object' && v !== null
565
864
 
865
+ const toMarketContextString = (value: unknown): string | undefined =>
866
+ typeof value === 'string' || typeof value === 'number'
867
+ ? String(value)
868
+ : undefined
869
+
870
+ const toMarketCapString = (
871
+ price: unknown,
872
+ circulatingSupply: unknown
873
+ ): string | undefined => {
874
+ const priceString = toMarketContextString(price)
875
+ const supplyString = toMarketContextString(circulatingSupply)
876
+ if (priceString === undefined || supplyString === undefined) {
877
+ return undefined
878
+ }
879
+
880
+ try {
881
+ const parsedPrice = new Big(priceString)
882
+ const parsedSupply = new Big(supplyString)
883
+ if (parsedPrice.lte(0) || parsedSupply.lte(0)) {
884
+ return undefined
885
+ }
886
+ return parsedPrice.times(parsedSupply).toFixed()
887
+ } catch {
888
+ return undefined
889
+ }
890
+ }
891
+
892
+ function mergePerpAssetCtx(
893
+ previous: HlWsPerpAssetCtx | undefined,
894
+ marketId: string,
895
+ update: HlWsPerpAssetCtxPayload
896
+ ): HlWsPerpAssetCtx | undefined {
897
+ const funding = update.funding ?? previous?.funding
898
+ const openInterest = update.openInterest ?? previous?.openInterest
899
+ const dayNtlVlm = update.dayNtlVlm ?? previous?.dayNtlVlm
900
+ const prevDayPx = update.prevDayPx ?? previous?.prevDayPx
901
+ const markPx = update.markPx ?? previous?.markPx
902
+ const midPx = 'midPx' in update ? update.midPx : previous?.midPx
903
+ const oraclePx = update.oraclePx ?? previous?.oraclePx
904
+
905
+ if (
906
+ funding === undefined ||
907
+ openInterest === undefined ||
908
+ dayNtlVlm === undefined ||
909
+ prevDayPx === undefined ||
910
+ markPx === undefined ||
911
+ midPx === undefined ||
912
+ oraclePx === undefined
913
+ ) {
914
+ return undefined
915
+ }
916
+
917
+ return {
918
+ coin: marketId,
919
+ funding,
920
+ openInterest,
921
+ dayNtlVlm,
922
+ prevDayPx,
923
+ markPx,
924
+ midPx,
925
+ oraclePx,
926
+ }
927
+ }
928
+
929
+ function allDexsAssetCtxEntries(
930
+ data: HlWsAllDexsAssetCtxsData
931
+ ): [string, HlWsPerpAssetCtxPayload[]][] {
932
+ return data.assetCtxs ?? data.ctxs ?? []
933
+ }
934
+
935
+ function compactRemovalPrice(
936
+ levels: OrderbookLevel[],
937
+ value: number | string | { p: string }
938
+ ): string | undefined {
939
+ if (typeof value === 'number') {
940
+ return levels[value]?.price
941
+ }
942
+ return typeof value === 'string' ? value : value.p
943
+ }
944
+
945
+ function applyCompressedL2Side(
946
+ previous: OrderbookLevel[],
947
+ updates: HlWsCompressedL2Data['l'][number],
948
+ removals: NonNullable<HlWsCompressedL2Data['r']>[number],
949
+ side: 'bid' | 'ask'
950
+ ): OrderbookLevel[] {
951
+ const byPrice = new Map(previous.map((level) => [level.price, level.size]))
952
+ for (const removal of removals) {
953
+ const price = compactRemovalPrice(previous, removal)
954
+ if (price !== undefined) {
955
+ byPrice.delete(price)
956
+ }
957
+ }
958
+ for (const update of updates) {
959
+ if (Number(update.s) === 0) {
960
+ byPrice.delete(update.p)
961
+ } else {
962
+ byPrice.set(update.p, update.s)
963
+ }
964
+ }
965
+
966
+ return [...byPrice.entries()]
967
+ .map(([price, size]) => ({ price, size }))
968
+ .sort((a, b) =>
969
+ side === 'bid'
970
+ ? Number(b.price) - Number(a.price)
971
+ : Number(a.price) - Number(b.price)
972
+ )
973
+ .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
974
+ }
975
+
566
976
  /**
567
977
  * Minimal presence/type check of the channel-discriminating and required
568
978
  * fields the matching handler dereferences without its own guard. A frame
@@ -572,7 +982,7 @@ const isObject = (v: unknown): v is Record<string, unknown> =>
572
982
  */
573
983
  function isValidHlFrame(channel: string, data: unknown): boolean {
574
984
  // `fastAssetCtxs` carries a base64 string payload, not an object.
575
- if (channel === 'fastAssetCtxs') {
985
+ if (channel === 'fastAssetCtxs' || channel === 'pac' || channel === 'sac') {
576
986
  return typeof data === 'string'
577
987
  }
578
988
  if (!isObject(data)) {
@@ -580,7 +990,7 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
580
990
  }
581
991
  switch (channel) {
582
992
  case 'allDexsAssetCtxs':
583
- return Array.isArray(data.assetCtxs)
993
+ return Array.isArray(data.assetCtxs) || Array.isArray(data.ctxs)
584
994
  case 'l2Book':
585
995
  return (
586
996
  typeof data.coin === 'string' &&
@@ -589,8 +999,12 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
589
999
  Array.isArray(data.levels[1]) &&
590
1000
  typeof data.time === 'number'
591
1001
  )
1002
+ case 'l2':
1003
+ return typeof data.c === 'string' || isObject(data.s) || isObject(data.u)
592
1004
  case 'candle':
593
1005
  return typeof data.s === 'string' && typeof data.i === 'string'
1006
+ case 'trades':
1007
+ return Array.isArray(data)
594
1008
  case 'orderUpdates':
595
1009
  return Array.isArray(data)
596
1010
  case 'userFills':