@lifi/perps-sdk-provider-hyperliquid 1.2.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.
- package/dist/cjs/types/ws.d.ts +15 -1
- package/dist/cjs/types/ws.d.ts.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +7 -0
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.js +153 -7
- package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/esm/types/ws.d.ts +28 -1
- package/dist/esm/types/ws.d.ts.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts +12 -8
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.js +160 -21
- package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/types/types/ws.d.ts +28 -1
- package/dist/types/types/ws.d.ts.map +1 -1
- package/dist/types/websocket/HyperliquidWsProvider.d.ts +12 -8
- package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/types/ws.ts +32 -1
- package/src/websocket/HyperliquidWsProvider.ts +193 -21
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
type Subscription,
|
|
22
22
|
type TriggerOrder,
|
|
23
23
|
} from '@lifi/perps-types'
|
|
24
|
-
import
|
|
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
|
|
99
|
-
//
|
|
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
|
|
146
|
-
//
|
|
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:
|
|
243
|
-
* perp
|
|
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: '
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
485
|
-
*
|
|
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
|
|
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' &&
|