@lifi/perps-sdk-provider-hyperliquid 1.2.0 → 1.3.1

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.
package/src/types/ws.ts CHANGED
@@ -4,6 +4,8 @@ import type { HlClearinghouseState, HlSpotBalance } from './account.js'
4
4
  import type { HlCandle, HlL2Book } from './asset.js'
5
5
  import type { HlUserFill } from './fill.js'
6
6
 
7
+ type HlWsNumberString = string | number
8
+
7
9
  /**
8
10
  * Perp asset context as carried on the `allDexsAssetCtxs` WS feed. Extends the
9
11
  * REST `/info` `HlAssetCtx` shape with the wire-only `coin` tag, the order-book
@@ -21,15 +23,27 @@ export type HlWsPerpAssetCtx = {
21
23
  oraclePx: string
22
24
  }
23
25
 
26
+ /** @public */
27
+ export type HlWsPerpAssetCtxPayload = Partial<HlWsPerpAssetCtx>
28
+
24
29
  /**
25
30
  * `allDexsAssetCtxs` carries one `[dex, ctxs]` pair per perp sub-DEX, mirroring
26
31
  * `allDexsClearinghouseState`. Spot contexts arrive separately.
27
32
  * @public
28
33
  */
29
34
  export type HlWsAllDexsAssetCtxsData = {
30
- assetCtxs: [string, HlWsPerpAssetCtx[]][]
35
+ assetCtxs?: [string, HlWsPerpAssetCtx[]][]
36
+ ctxs?: [string, HlWsPerpAssetCtxPayload[]][]
31
37
  }
32
38
 
39
+ /**
40
+ * Compressed perp asset-context feed. Snapshot/update frames carry one
41
+ * `[dex, ctxs]` pair per perp sub-DEX. Updates can omit unchanged fields inside
42
+ * each context.
43
+ * @public
44
+ */
45
+ export type HlWsPacData = [string, HlWsPerpAssetCtxPayload[]][]
46
+
33
47
  /** @public */
34
48
  export type HlWsMessage = {
35
49
  channel: string
@@ -47,6 +61,23 @@ export type HlWsFastAssetCtx = {
47
61
  midPx?: string | null
48
62
  }
49
63
 
64
+ /** @public */
65
+ export type HlWsSpotAssetCtx = {
66
+ dayNtlVlm?: HlWsNumberString
67
+ prevDayPx?: HlWsNumberString
68
+ markPx?: HlWsNumberString
69
+ midPx?: HlWsNumberString | null
70
+ dayBaseVlm?: HlWsNumberString
71
+ circulatingSupply?: HlWsNumberString
72
+ }
73
+
74
+ /**
75
+ * Compressed spot asset-context feed. The first frame is a broad snapshot; later
76
+ * frames only carry changed fields.
77
+ * @public
78
+ */
79
+ export type HlWsSacData = Record<string, HlWsSpotAssetCtx>
80
+
50
81
  /** @public */
51
82
  export type HlWsL2BookData = HlL2Book & { coin: string }
52
83
 
@@ -21,7 +21,8 @@ import {
21
21
  type Subscription,
22
22
  type TriggerOrder,
23
23
  } from '@lifi/perps-types'
24
- 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'
25
26
  import type {
26
27
  HlAssetPosition,
27
28
  HlOrderDetail,
@@ -34,7 +35,11 @@ import type {
34
35
  HlWsL2BookData,
35
36
  HlWsL2Data,
36
37
  HlWsMessage,
38
+ HlWsPacData,
37
39
  HlWsPerpAssetCtx,
40
+ HlWsPerpAssetCtxPayload,
41
+ HlWsSacData,
42
+ HlWsSpotAssetCtx,
38
43
  HlWsSpotStateData,
39
44
  HlWsTrade,
40
45
  HlWsUserFillsData,
@@ -91,16 +96,19 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
91
96
  private readonly client: PerpsSDKClient | undefined
92
97
  private readonly registry: MarketRegistry | undefined
93
98
  private perpCtxBySubDex = new Map<string, Record<string, HlWsPerpAssetCtx>>()
99
+ private spotCtxByMarketId: Record<string, HlWsSpotAssetCtx> = {}
94
100
  private orderbookKeysByMarketId = new Map<string, Set<string>>()
95
101
  private latestOrderbookByMarketId = new Map<string, OrderbookResponse>()
96
102
  // Latest mid/mark per `Market.id` from the high-frequency `fastAssetCtxs`
97
103
  // 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).
104
+ // frames carry only changed coins — and overlaid onto asset contexts that
105
+ // carry oracle/funding/OI/metadata.
100
106
  private fastCtxByMarketId: Record<string, HlWsFastAssetCtx> = {}
101
107
  // `fastAssetCtxs` payloads are base64 + raw-DEFLATE; decode is async, so
102
108
  // chain decodes to apply incremental frames in arrival order.
103
109
  private fastDecodeChain: Promise<void> = Promise.resolve()
110
+ private pacDecodeChain: Promise<void> = Promise.resolve()
111
+ private sacDecodeChain: Promise<void> = Promise.resolve()
104
112
  private orderbookDecodeChain: Promise<void> = Promise.resolve()
105
113
 
106
114
  constructor(wsUrl: string, providerKey: string, client?: PerpsSDKClient) {
@@ -142,9 +150,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
142
150
 
143
151
  await this.registry?.sync()
144
152
 
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.
153
+ // Markets context aggregates slower asset-context feeds with fast mid/mark
154
+ // ticks from `fastAssetCtxs`.
148
155
  if (sub.channel === 'marketsContext') {
149
156
  const entries = this.getMarketsContextSubEntries()
150
157
 
@@ -162,6 +169,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
162
169
  )
163
170
  }
164
171
  this.perpCtxBySubDex.clear()
172
+ this.spotCtxByMarketId = {}
165
173
  this.fastCtxByMarketId = {}
166
174
  }
167
175
  }
@@ -239,8 +247,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
239
247
  }
240
248
 
241
249
  /**
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
250
+ * Sub-key + payload pairs for the markets-context aggregation: compressed
251
+ * all-perp (`pac`) and all-spot (`sac`) asset contexts plus the documented
244
252
  * `fastAssetCtxs` feed (high-frequency mid + mark across every dex).
245
253
  */
246
254
  private getMarketsContextSubEntries(): Array<{
@@ -248,7 +256,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
248
256
  payload: object
249
257
  }> {
250
258
  return [
251
- { subKey: 'allDexsAssetCtxs', payload: { type: 'allDexsAssetCtxs' } },
259
+ { subKey: 'pac', payload: { type: 'pac' } },
260
+ { subKey: 'sac', payload: { type: 'sac' } },
252
261
  { subKey: 'fastAssetCtxs', payload: { type: 'fastAssetCtxs' } },
253
262
  ]
254
263
  }
@@ -279,9 +288,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
279
288
 
280
289
  protected override onClose(): void {
281
290
  this.perpCtxBySubDex.clear()
291
+ this.spotCtxByMarketId = {}
282
292
  this.fastCtxByMarketId = {}
283
293
  this.orderbookKeysByMarketId.clear()
284
294
  this.latestOrderbookByMarketId.clear()
295
+ this.fastDecodeChain = Promise.resolve()
296
+ this.pacDecodeChain = Promise.resolve()
297
+ this.sacDecodeChain = Promise.resolve()
298
+ this.orderbookDecodeChain = Promise.resolve()
285
299
  this.orderUpdatesKey = undefined
286
300
  }
287
301
 
@@ -412,6 +426,12 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
412
426
  case 'allDexsAssetCtxs':
413
427
  this.handleAllDexsAssetCtxs(msg.data as HlWsAllDexsAssetCtxsData)
414
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
415
435
  case 'fastAssetCtxs':
416
436
  this.handleFastAssetCtxs(msg.data as string)
417
437
  break
@@ -448,14 +468,76 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
448
468
  }
449
469
 
450
470
  private handleAllDexsAssetCtxs(data: HlWsAllDexsAssetCtxsData) {
451
- for (const [dex, ctxs] of data.assetCtxs) {
452
- 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
+ }
453
494
  for (const ctx of ctxs) {
454
- 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
+ }
455
503
  }
456
504
  this.perpCtxBySubDex.set(dex || 'default', byMarketId)
457
505
  }
458
- 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
459
541
  }
460
542
 
461
543
  /**
@@ -481,11 +563,8 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
481
563
  }
482
564
 
483
565
  /**
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.
566
+ * Build the all-markets context map from perp + spot asset contexts, with mid
567
+ * + mark overlaid from `fastAssetCtxs`.
489
568
  */
490
569
  private emitMarketsContext() {
491
570
  const data: Record<string, MarketContext> = {}
@@ -500,6 +579,23 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
500
579
  }
501
580
  }
502
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
+ }
503
599
  for (const [marketId, fast] of Object.entries(this.fastCtxByMarketId)) {
504
600
  if (data[marketId] !== undefined) {
505
601
  continue
@@ -515,7 +611,7 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
515
611
  this.emit('marketsContext', { channel: 'marketsContext', data })
516
612
  }
517
613
 
518
- /** Latest mid per `Market.id` across the perp ctx feed and `fastAssetCtxs`. */
614
+ /** Latest mid per `Market.id` across asset-context and fast feeds. */
519
615
  private mergedMids(): Map<string, number> {
520
616
  const map = new Map<string, number>()
521
617
  for (const byMarketId of this.perpCtxBySubDex.values()) {
@@ -524,6 +620,12 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
524
620
  map.set(id, Number(mid))
525
621
  }
526
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
+ }
527
629
  for (const [id, fast] of Object.entries(this.fastCtxByMarketId)) {
528
630
  const mid = fast.midPx ?? fast.markPx
529
631
  if (mid != null) {
@@ -760,6 +862,76 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
760
862
  const isObject = (v: unknown): v is Record<string, unknown> =>
761
863
  typeof v === 'object' && v !== null
762
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
+
763
935
  function compactRemovalPrice(
764
936
  levels: OrderbookLevel[],
765
937
  value: number | string | { p: string }
@@ -810,7 +982,7 @@ function applyCompressedL2Side(
810
982
  */
811
983
  function isValidHlFrame(channel: string, data: unknown): boolean {
812
984
  // `fastAssetCtxs` carries a base64 string payload, not an object.
813
- if (channel === 'fastAssetCtxs') {
985
+ if (channel === 'fastAssetCtxs' || channel === 'pac' || channel === 'sac') {
814
986
  return typeof data === 'string'
815
987
  }
816
988
  if (!isObject(data)) {
@@ -818,7 +990,7 @@ function isValidHlFrame(channel: string, data: unknown): boolean {
818
990
  }
819
991
  switch (channel) {
820
992
  case 'allDexsAssetCtxs':
821
- return Array.isArray(data.assetCtxs)
993
+ return Array.isArray(data.assetCtxs) || Array.isArray(data.ctxs)
822
994
  case 'l2Book':
823
995
  return (
824
996
  typeof data.coin === 'string' &&