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