@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.
- package/dist/cjs/types/ws.d.ts +48 -1
- package/dist/cjs/types/ws.d.ts.map +1 -1
- package/dist/cjs/utils/decodeFastAssetCtxs.d.ts +1 -0
- package/dist/cjs/utils/decodeFastAssetCtxs.d.ts.map +1 -1
- package/dist/cjs/utils/decodeFastAssetCtxs.js +5 -1
- package/dist/cjs/utils/decodeFastAssetCtxs.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +1 -1
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +2 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +20 -0
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.js +359 -29
- package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/esm/types/ws.d.ts +65 -1
- package/dist/esm/types/ws.d.ts.map +1 -1
- package/dist/esm/utils/decodeFastAssetCtxs.d.ts +1 -0
- package/dist/esm/utils/decodeFastAssetCtxs.d.ts.map +1 -1
- package/dist/esm/utils/decodeFastAssetCtxs.js +9 -6
- package/dist/esm/utils/decodeFastAssetCtxs.js.map +1 -1
- package/dist/esm/utils/index.d.ts +1 -1
- package/dist/esm/utils/index.d.ts.map +1 -1
- package/dist/esm/utils/index.js +1 -1
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts +25 -8
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.js +368 -48
- package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/types/types/ws.d.ts +65 -1
- package/dist/types/types/ws.d.ts.map +1 -1
- package/dist/types/utils/decodeFastAssetCtxs.d.ts +1 -0
- package/dist/types/utils/decodeFastAssetCtxs.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +1 -1
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/websocket/HyperliquidWsProvider.d.ts +25 -8
- package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/types/ws.ts +67 -1
- package/src/utils/decodeFastAssetCtxs.ts +10 -6
- package/src/utils/index.ts +4 -1
- 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
|
|
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 `
|
|
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
|
|
91
|
-
//
|
|
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
|
|
137
|
-
//
|
|
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({
|
|
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:
|
|
203
|
-
* 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
|
|
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: '
|
|
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 `
|
|
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: '
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
379
|
-
*
|
|
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
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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':
|