@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getMarketRegistry, isActiveOrderStatus, ReconnectingWebSocket, resolveSubscribeQuote, WsProviderBase, wsLog, } from '@lifi/perps-sdk';
|
|
2
2
|
import { OrderSide, OrderType, } from '@lifi/perps-types';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import Big from 'big.js';
|
|
4
|
+
import { HYPERLIQUID_FEE_TIER_FALLBACK, SPOT_MARKET_ID } from '../constants.js';
|
|
5
|
+
import { decodeCompressedJson, decodeFastAssetCtxs, isOpenAssetPosition, isTriggerOrder, mapFill, mapMarketContext, mapOrderStatus, mapOrderType, mapPosition, priceStepToAggregation, spotAssetFromToken, spotBalance, spotPriceById, } from '../utils/index.js';
|
|
6
|
+
/** HL's compact `l2` snapshot carries 20 levels per side. */
|
|
6
7
|
const HL_L2_BOOK_MAX_LEVELS_PER_SIDE = 20;
|
|
7
8
|
/**
|
|
8
9
|
* `WsProviderFactory` constructor for Hyperliquid — pass to
|
|
@@ -33,14 +34,20 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
33
34
|
client;
|
|
34
35
|
registry;
|
|
35
36
|
perpCtxBySubDex = new Map();
|
|
37
|
+
spotCtxByMarketId = {};
|
|
38
|
+
orderbookKeysByMarketId = new Map();
|
|
39
|
+
latestOrderbookByMarketId = new Map();
|
|
36
40
|
// Latest mid/mark per `Market.id` from the high-frequency `fastAssetCtxs`
|
|
37
41
|
// feed (all dexes, incl. builder/sub-dex coins). Merged incrementally —
|
|
38
|
-
// frames carry only changed coins — and overlaid onto
|
|
39
|
-
//
|
|
42
|
+
// frames carry only changed coins — and overlaid onto asset contexts that
|
|
43
|
+
// carry oracle/funding/OI/metadata.
|
|
40
44
|
fastCtxByMarketId = {};
|
|
41
45
|
// `fastAssetCtxs` payloads are base64 + raw-DEFLATE; decode is async, so
|
|
42
46
|
// chain decodes to apply incremental frames in arrival order.
|
|
43
47
|
fastDecodeChain = Promise.resolve();
|
|
48
|
+
pacDecodeChain = Promise.resolve();
|
|
49
|
+
sacDecodeChain = Promise.resolve();
|
|
50
|
+
orderbookDecodeChain = Promise.resolve();
|
|
44
51
|
constructor(wsUrl, providerKey, client) {
|
|
45
52
|
super(new ReconnectingWebSocket(wsUrl, { pingPayload: '{"method":"ping"}' }), providerKey);
|
|
46
53
|
this.client = client;
|
|
@@ -61,9 +68,8 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
61
68
|
this.claimOrderUpdatesKey(this.toKey(sub));
|
|
62
69
|
}
|
|
63
70
|
await this.registry?.sync();
|
|
64
|
-
// Markets context aggregates
|
|
65
|
-
//
|
|
66
|
-
// feed, which supplies fast mid + mark for every market across all dexes.
|
|
71
|
+
// Markets context aggregates slower asset-context feeds with fast mid/mark
|
|
72
|
+
// ticks from `fastAssetCtxs`.
|
|
67
73
|
if (sub.channel === 'marketsContext') {
|
|
68
74
|
const entries = this.getMarketsContextSubEntries();
|
|
69
75
|
for (const { subKey, payload } of entries) {
|
|
@@ -76,20 +82,33 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
76
82
|
this.rws.send(JSON.stringify({ method: 'unsubscribe', subscription: payload }));
|
|
77
83
|
}
|
|
78
84
|
this.perpCtxBySubDex.clear();
|
|
85
|
+
this.spotCtxByMarketId = {};
|
|
79
86
|
this.fastCtxByMarketId = {};
|
|
80
87
|
};
|
|
81
88
|
}
|
|
82
89
|
// All other channels: single WS subscription per key
|
|
83
90
|
const key = this.toKey(sub);
|
|
91
|
+
if (sub.channel === 'orderbook') {
|
|
92
|
+
this.claimOrderbookKey(sub.marketId, key);
|
|
93
|
+
}
|
|
84
94
|
const payload = this.toHlPayload(sub);
|
|
85
95
|
await this.registerSub(key, payload);
|
|
96
|
+
if (sub.channel === 'orderbook') {
|
|
97
|
+
this.registerOrderbook(sub.marketId, key);
|
|
98
|
+
}
|
|
86
99
|
await this.rws.ready();
|
|
87
100
|
return () => {
|
|
88
101
|
this.unregisterSub(key);
|
|
89
102
|
if (this.orderUpdatesKey === key) {
|
|
90
103
|
this.orderUpdatesKey = undefined;
|
|
91
104
|
}
|
|
92
|
-
this.rws.send(JSON.stringify({
|
|
105
|
+
this.rws.send(JSON.stringify({
|
|
106
|
+
method: 'unsubscribe',
|
|
107
|
+
subscription: payload,
|
|
108
|
+
}));
|
|
109
|
+
if (sub.channel === 'orderbook') {
|
|
110
|
+
this.releaseOrderbook(sub.marketId, key);
|
|
111
|
+
}
|
|
93
112
|
};
|
|
94
113
|
}
|
|
95
114
|
/**
|
|
@@ -110,20 +129,62 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
110
129
|
}
|
|
111
130
|
this.orderUpdatesKey = key;
|
|
112
131
|
}
|
|
132
|
+
claimOrderbookKey(marketId, key) {
|
|
133
|
+
const activeKeys = this.orderbookKeysByMarketId.get(marketId);
|
|
134
|
+
if (activeKeys === undefined) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const activeKey of [...activeKeys]) {
|
|
138
|
+
if (activeKey === key) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!this.closeChannelIfIdle(activeKey)) {
|
|
142
|
+
throw new Error(`Hyperliquid supports one orderbook aggregation per market per ` +
|
|
143
|
+
`provider instance. Unsubscribe ${activeKey} before subscribing ${key}.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
113
147
|
/**
|
|
114
|
-
* Sub-key + payload pairs for the markets-context aggregation:
|
|
115
|
-
* perp
|
|
148
|
+
* Sub-key + payload pairs for the markets-context aggregation: compressed
|
|
149
|
+
* all-perp (`pac`) and all-spot (`sac`) asset contexts plus the documented
|
|
116
150
|
* `fastAssetCtxs` feed (high-frequency mid + mark across every dex).
|
|
117
151
|
*/
|
|
118
152
|
getMarketsContextSubEntries() {
|
|
119
153
|
return [
|
|
120
|
-
{ subKey: '
|
|
154
|
+
{ subKey: 'pac', payload: { type: 'pac' } },
|
|
155
|
+
{ subKey: 'sac', payload: { type: 'sac' } },
|
|
121
156
|
{ subKey: 'fastAssetCtxs', payload: { type: 'fastAssetCtxs' } },
|
|
122
157
|
];
|
|
123
158
|
}
|
|
159
|
+
registerOrderbook(marketId, orderbookKey) {
|
|
160
|
+
let activeKeys = this.orderbookKeysByMarketId.get(marketId);
|
|
161
|
+
if (activeKeys === undefined) {
|
|
162
|
+
activeKeys = new Set();
|
|
163
|
+
this.orderbookKeysByMarketId.set(marketId, activeKeys);
|
|
164
|
+
}
|
|
165
|
+
activeKeys.add(orderbookKey);
|
|
166
|
+
}
|
|
167
|
+
releaseOrderbook(marketId, orderbookKey) {
|
|
168
|
+
const activeKeys = this.orderbookKeysByMarketId.get(marketId);
|
|
169
|
+
if (activeKeys === undefined) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
activeKeys.delete(orderbookKey);
|
|
173
|
+
if (activeKeys.size > 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.orderbookKeysByMarketId.delete(marketId);
|
|
177
|
+
}
|
|
124
178
|
onClose() {
|
|
125
179
|
this.perpCtxBySubDex.clear();
|
|
180
|
+
this.spotCtxByMarketId = {};
|
|
126
181
|
this.fastCtxByMarketId = {};
|
|
182
|
+
this.orderbookKeysByMarketId.clear();
|
|
183
|
+
this.latestOrderbookByMarketId.clear();
|
|
184
|
+
this.fastDecodeChain = Promise.resolve();
|
|
185
|
+
this.pacDecodeChain = Promise.resolve();
|
|
186
|
+
this.sacDecodeChain = Promise.resolve();
|
|
187
|
+
this.orderbookDecodeChain = Promise.resolve();
|
|
127
188
|
this.orderUpdatesKey = undefined;
|
|
128
189
|
}
|
|
129
190
|
sendSubscribe(payload) {
|
|
@@ -134,9 +195,11 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
134
195
|
case 'marketsContext':
|
|
135
196
|
return 'marketsContext';
|
|
136
197
|
case 'orderbook':
|
|
137
|
-
return `
|
|
198
|
+
return `l2:${sub.marketId}:${this.orderbookAggregationKey(sub.marketId, sub.priceStep)}`;
|
|
138
199
|
case 'candle':
|
|
139
200
|
return `candle:${sub.marketId}:${sub.interval}`;
|
|
201
|
+
case 'trades':
|
|
202
|
+
return `trades:${sub.marketId}`;
|
|
140
203
|
case 'orderUpdates':
|
|
141
204
|
return `orderUpdates:${sub.address.toLowerCase()}`;
|
|
142
205
|
case 'fills':
|
|
@@ -147,29 +210,47 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
147
210
|
return `spotState:${sub.address.toLowerCase()}`;
|
|
148
211
|
}
|
|
149
212
|
}
|
|
213
|
+
orderbookAggregation(marketId, priceStep) {
|
|
214
|
+
if (priceStep === undefined) {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
return priceStepToAggregation(priceStep, this.orderbookReferencePrice(marketId));
|
|
218
|
+
}
|
|
219
|
+
orderbookAggregationKey(marketId, priceStep) {
|
|
220
|
+
const aggregation = this.orderbookAggregation(marketId, priceStep);
|
|
221
|
+
const nSigFigs = aggregation.nSigFigs ?? null;
|
|
222
|
+
const mantissa = aggregation.mantissa ?? null;
|
|
223
|
+
if (nSigFigs === null && mantissa === null) {
|
|
224
|
+
return 'full';
|
|
225
|
+
}
|
|
226
|
+
return `s:${nSigFigs}:m:${mantissa}`;
|
|
227
|
+
}
|
|
150
228
|
toHlPayload(sub) {
|
|
151
229
|
switch (sub.channel) {
|
|
152
230
|
case 'marketsContext':
|
|
153
231
|
// Handled via getMarketsContextSubEntries in openChannel; never reaches
|
|
154
232
|
// toHlPayload, but TS requires an exhaustive switch.
|
|
155
233
|
return { type: 'allDexsAssetCtxs' };
|
|
156
|
-
case 'orderbook':
|
|
234
|
+
case 'orderbook': {
|
|
235
|
+
const aggregation = this.orderbookAggregation(sub.marketId, sub.priceStep);
|
|
157
236
|
return {
|
|
158
|
-
type: '
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// valid only when nSigFigs === 5).
|
|
163
|
-
...(sub.priceStep !== undefined
|
|
164
|
-
? priceStepToAggregation(sub.priceStep, this.mergedMids().get(sub.marketId) ?? Number.NaN)
|
|
165
|
-
: {}),
|
|
237
|
+
type: 'l2',
|
|
238
|
+
c: sub.marketId,
|
|
239
|
+
s: aggregation.nSigFigs ?? null,
|
|
240
|
+
m: aggregation.mantissa ?? null,
|
|
166
241
|
};
|
|
242
|
+
}
|
|
167
243
|
case 'candle':
|
|
168
244
|
return {
|
|
169
245
|
type: 'candle',
|
|
170
246
|
coin: sub.marketId,
|
|
171
247
|
interval: sub.interval,
|
|
172
248
|
};
|
|
249
|
+
case 'trades':
|
|
250
|
+
return {
|
|
251
|
+
type: 'trades',
|
|
252
|
+
coin: sub.marketId,
|
|
253
|
+
};
|
|
173
254
|
case 'orderUpdates':
|
|
174
255
|
return { type: 'orderUpdates', user: sub.address };
|
|
175
256
|
case 'fills':
|
|
@@ -207,15 +288,27 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
207
288
|
case 'allDexsAssetCtxs':
|
|
208
289
|
this.handleAllDexsAssetCtxs(msg.data);
|
|
209
290
|
break;
|
|
291
|
+
case 'pac':
|
|
292
|
+
this.handlePac(msg.data);
|
|
293
|
+
break;
|
|
294
|
+
case 'sac':
|
|
295
|
+
this.handleSac(msg.data);
|
|
296
|
+
break;
|
|
210
297
|
case 'fastAssetCtxs':
|
|
211
298
|
this.handleFastAssetCtxs(msg.data);
|
|
212
299
|
break;
|
|
300
|
+
case 'l2':
|
|
301
|
+
this.handleL2(msg.data);
|
|
302
|
+
break;
|
|
213
303
|
case 'l2Book':
|
|
214
304
|
this.handleL2Book(msg.data);
|
|
215
305
|
break;
|
|
216
306
|
case 'candle':
|
|
217
307
|
this.handleCandle(msg.data);
|
|
218
308
|
break;
|
|
309
|
+
case 'trades':
|
|
310
|
+
this.handleTrades(msg.data);
|
|
311
|
+
break;
|
|
219
312
|
case 'orderUpdates':
|
|
220
313
|
this.handleOrderUpdates(msg.data);
|
|
221
314
|
break;
|
|
@@ -235,14 +328,65 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
235
328
|
}
|
|
236
329
|
}
|
|
237
330
|
handleAllDexsAssetCtxs(data) {
|
|
238
|
-
|
|
239
|
-
|
|
331
|
+
this.mergePerpAssetCtxEntries(allDexsAssetCtxEntries(data));
|
|
332
|
+
this.emitMarketsContext();
|
|
333
|
+
}
|
|
334
|
+
handlePac(base64) {
|
|
335
|
+
this.pacDecodeChain = this.pacDecodeChain
|
|
336
|
+
.then(async () => {
|
|
337
|
+
this.mergePerpAssetCtxEntries(await decodeCompressedJson(base64));
|
|
338
|
+
this.emitMarketsContext();
|
|
339
|
+
})
|
|
340
|
+
.catch((error) => wsLog.handlerFailure(this.providerKey, error));
|
|
341
|
+
}
|
|
342
|
+
mergePerpAssetCtxEntries(entries) {
|
|
343
|
+
for (const [dex, ctxs] of entries) {
|
|
344
|
+
const previous = this.perpCtxBySubDex.get(dex || 'default');
|
|
345
|
+
const byMarketId = {
|
|
346
|
+
...(previous ?? {}),
|
|
347
|
+
};
|
|
240
348
|
for (const ctx of ctxs) {
|
|
241
|
-
|
|
349
|
+
const marketId = ctx.coin;
|
|
350
|
+
if (marketId === undefined) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const merged = mergePerpAssetCtx(previous?.[marketId], marketId, ctx);
|
|
354
|
+
if (merged !== undefined) {
|
|
355
|
+
byMarketId[marketId] = merged;
|
|
356
|
+
}
|
|
242
357
|
}
|
|
243
358
|
this.perpCtxBySubDex.set(dex || 'default', byMarketId);
|
|
244
359
|
}
|
|
245
|
-
|
|
360
|
+
}
|
|
361
|
+
handleSac(base64) {
|
|
362
|
+
this.sacDecodeChain = this.sacDecodeChain
|
|
363
|
+
.then(async () => {
|
|
364
|
+
const spotMarketIdBySacKey = this.spotMarketIdBySacKey();
|
|
365
|
+
const ctxs = await decodeCompressedJson(base64);
|
|
366
|
+
for (const [sacKey, ctx] of Object.entries(ctxs)) {
|
|
367
|
+
const marketId = spotMarketIdBySacKey.get(sacKey);
|
|
368
|
+
if (marketId === undefined) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
this.spotCtxByMarketId[marketId] = {
|
|
372
|
+
...this.spotCtxByMarketId[marketId],
|
|
373
|
+
...ctx,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
this.emitMarketsContext();
|
|
377
|
+
})
|
|
378
|
+
.catch((error) => wsLog.handlerFailure(this.providerKey, error));
|
|
379
|
+
}
|
|
380
|
+
spotMarketIdBySacKey() {
|
|
381
|
+
const map = new Map();
|
|
382
|
+
for (const market of this.registry?.markets ?? []) {
|
|
383
|
+
if (market.categoryId !== SPOT_MARKET_ID) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
map.set(market.id, market.id);
|
|
387
|
+
map.set(`${market.baseAsset.displaySymbol}/${market.quoteAsset.displaySymbol}`, market.id);
|
|
388
|
+
}
|
|
389
|
+
return map;
|
|
246
390
|
}
|
|
247
391
|
/**
|
|
248
392
|
* Decode a `fastAssetCtxs` frame (base64 + raw-DEFLATE) and merge its changed
|
|
@@ -266,11 +410,8 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
266
410
|
.catch((error) => wsLog.handlerFailure(this.providerKey, error));
|
|
267
411
|
}
|
|
268
412
|
/**
|
|
269
|
-
* Build the all-markets context map
|
|
270
|
-
*
|
|
271
|
-
* high-frequency `fastAssetCtxs` feed. A market present in `fastAssetCtxs` but
|
|
272
|
-
* without an asset-context frame yet (the aggregate feed is slow/idle) still
|
|
273
|
-
* gets a mid+mark entry, so price ticks without waiting on the aggregate.
|
|
413
|
+
* Build the all-markets context map from perp + spot asset contexts, with mid
|
|
414
|
+
* + mark overlaid from `fastAssetCtxs`.
|
|
274
415
|
*/
|
|
275
416
|
emitMarketsContext() {
|
|
276
417
|
const data = {};
|
|
@@ -285,6 +426,23 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
285
426
|
};
|
|
286
427
|
}
|
|
287
428
|
}
|
|
429
|
+
for (const [marketId, ctx] of Object.entries(this.spotCtxByMarketId)) {
|
|
430
|
+
const midPrice = toMarketContextString(ctx.midPx ?? ctx.markPx);
|
|
431
|
+
const markPrice = toMarketContextString(ctx.markPx);
|
|
432
|
+
if (midPrice === undefined || markPrice === undefined) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const fast = this.fastCtxByMarketId[marketId];
|
|
436
|
+
const emittedMarkPrice = fast?.markPx != null ? fast.markPx : markPrice;
|
|
437
|
+
data[marketId] = {
|
|
438
|
+
marketId,
|
|
439
|
+
midPrice: fast?.midPx != null ? fast.midPx : midPrice,
|
|
440
|
+
markPrice: emittedMarkPrice,
|
|
441
|
+
prevDayPrice: toMarketContextString(ctx.prevDayPx),
|
|
442
|
+
volume24h: toMarketContextString(ctx.dayNtlVlm),
|
|
443
|
+
marketCap: toMarketCapString(emittedMarkPrice, ctx.circulatingSupply),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
288
446
|
for (const [marketId, fast] of Object.entries(this.fastCtxByMarketId)) {
|
|
289
447
|
if (data[marketId] !== undefined) {
|
|
290
448
|
continue;
|
|
@@ -299,7 +457,7 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
299
457
|
}
|
|
300
458
|
this.emit('marketsContext', { channel: 'marketsContext', data });
|
|
301
459
|
}
|
|
302
|
-
/** Latest mid per `Market.id` across
|
|
460
|
+
/** Latest mid per `Market.id` across asset-context and fast feeds. */
|
|
303
461
|
mergedMids() {
|
|
304
462
|
const map = new Map();
|
|
305
463
|
for (const byMarketId of this.perpCtxBySubDex.values()) {
|
|
@@ -308,6 +466,12 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
308
466
|
map.set(id, Number(mid));
|
|
309
467
|
}
|
|
310
468
|
}
|
|
469
|
+
for (const [id, ctx] of Object.entries(this.spotCtxByMarketId)) {
|
|
470
|
+
const mid = ctx.midPx ?? ctx.markPx;
|
|
471
|
+
if (mid != null) {
|
|
472
|
+
map.set(id, Number(mid));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
311
475
|
for (const [id, fast] of Object.entries(this.fastCtxByMarketId)) {
|
|
312
476
|
const mid = fast.midPx ?? fast.markPx;
|
|
313
477
|
if (mid != null) {
|
|
@@ -316,21 +480,74 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
316
480
|
}
|
|
317
481
|
return map;
|
|
318
482
|
}
|
|
483
|
+
orderbookReferencePrice(marketId) {
|
|
484
|
+
const mid = this.mergedMids().get(marketId);
|
|
485
|
+
if (mid !== undefined) {
|
|
486
|
+
return mid;
|
|
487
|
+
}
|
|
488
|
+
const book = this.latestOrderbookByMarketId.get(marketId);
|
|
489
|
+
const bid = book?.bids[0]?.price;
|
|
490
|
+
const ask = book?.asks[0]?.price;
|
|
491
|
+
if (bid !== undefined && ask !== undefined) {
|
|
492
|
+
return (Number(bid) + Number(ask)) / 2;
|
|
493
|
+
}
|
|
494
|
+
return Number.NaN;
|
|
495
|
+
}
|
|
319
496
|
handleL2Book(data) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
497
|
+
const book = {
|
|
498
|
+
provider: this.providerKey,
|
|
499
|
+
marketId: data.coin,
|
|
500
|
+
bids: data.levels[0]
|
|
501
|
+
.slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
|
|
502
|
+
.map((l) => ({ price: l.px, size: l.sz })),
|
|
503
|
+
asks: data.levels[1]
|
|
504
|
+
.slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
|
|
505
|
+
.map((l) => ({ price: l.px, size: l.sz })),
|
|
506
|
+
timestamp: data.time,
|
|
507
|
+
};
|
|
508
|
+
this.latestOrderbookByMarketId.set(data.coin, book);
|
|
509
|
+
this.emitOrderbook(data.coin, book);
|
|
510
|
+
}
|
|
511
|
+
handleL2(data) {
|
|
512
|
+
if (data.s !== undefined) {
|
|
513
|
+
this.handleL2Book(data.s);
|
|
514
|
+
}
|
|
515
|
+
if (data.u !== undefined) {
|
|
516
|
+
this.handleCompactL2Delta(data.u);
|
|
517
|
+
}
|
|
518
|
+
if (data.c === undefined) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const compressed = data.c;
|
|
522
|
+
this.orderbookDecodeChain = this.orderbookDecodeChain
|
|
523
|
+
.then(async () => {
|
|
524
|
+
this.handleCompactL2Delta(await decodeCompressedJson(compressed));
|
|
525
|
+
})
|
|
526
|
+
.catch((error) => wsLog.handlerFailure(this.providerKey, error));
|
|
527
|
+
}
|
|
528
|
+
handleCompactL2Delta(delta) {
|
|
529
|
+
const previous = this.latestOrderbookByMarketId.get(delta.c);
|
|
530
|
+
if (previous === undefined) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const book = {
|
|
534
|
+
provider: this.providerKey,
|
|
535
|
+
marketId: delta.c,
|
|
536
|
+
bids: applyCompressedL2Side(previous.bids, delta.l[0], delta.r?.[0] ?? [], 'bid'),
|
|
537
|
+
asks: applyCompressedL2Side(previous.asks, delta.l[1], delta.r?.[1] ?? [], 'ask'),
|
|
538
|
+
timestamp: delta.t,
|
|
539
|
+
};
|
|
540
|
+
this.latestOrderbookByMarketId.set(delta.c, book);
|
|
541
|
+
this.emitOrderbook(delta.c, book);
|
|
542
|
+
}
|
|
543
|
+
emitOrderbook(marketId, book) {
|
|
544
|
+
const keys = this.orderbookKeysByMarketId.get(marketId);
|
|
545
|
+
if (keys === undefined) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
for (const key of keys) {
|
|
549
|
+
this.emit(key, { channel: 'orderbook', data: book });
|
|
550
|
+
}
|
|
334
551
|
}
|
|
335
552
|
handleCandle(data) {
|
|
336
553
|
this.emit(`candle:${data.s}:${data.i}`, {
|
|
@@ -345,6 +562,24 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
345
562
|
},
|
|
346
563
|
});
|
|
347
564
|
}
|
|
565
|
+
handleTrades(data) {
|
|
566
|
+
for (const trade of data) {
|
|
567
|
+
this.emit(`trades:${trade.coin}`, {
|
|
568
|
+
channel: 'trades',
|
|
569
|
+
data: [
|
|
570
|
+
{
|
|
571
|
+
provider: this.providerKey,
|
|
572
|
+
marketId: trade.coin,
|
|
573
|
+
price: trade.px,
|
|
574
|
+
size: trade.sz,
|
|
575
|
+
timestamp: trade.time,
|
|
576
|
+
side: trade.side === 'B' ? 'buy' : 'sell',
|
|
577
|
+
id: trade.tid !== undefined ? String(trade.tid) : trade.hash,
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
348
583
|
handleOrderUpdates(data) {
|
|
349
584
|
// Untagged frame: attributable only via the single-address invariant.
|
|
350
585
|
const key = this.orderUpdatesKey;
|
|
@@ -437,6 +672,87 @@ export class HyperliquidWsProvider extends WsProviderBase {
|
|
|
437
672
|
}
|
|
438
673
|
}
|
|
439
674
|
const isObject = (v) => typeof v === 'object' && v !== null;
|
|
675
|
+
const toMarketContextString = (value) => typeof value === 'string' || typeof value === 'number'
|
|
676
|
+
? String(value)
|
|
677
|
+
: undefined;
|
|
678
|
+
const toMarketCapString = (price, circulatingSupply) => {
|
|
679
|
+
const priceString = toMarketContextString(price);
|
|
680
|
+
const supplyString = toMarketContextString(circulatingSupply);
|
|
681
|
+
if (priceString === undefined || supplyString === undefined) {
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const parsedPrice = new Big(priceString);
|
|
686
|
+
const parsedSupply = new Big(supplyString);
|
|
687
|
+
if (parsedPrice.lte(0) || parsedSupply.lte(0)) {
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
return parsedPrice.times(parsedSupply).toFixed();
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
function mergePerpAssetCtx(previous, marketId, update) {
|
|
697
|
+
const funding = update.funding ?? previous?.funding;
|
|
698
|
+
const openInterest = update.openInterest ?? previous?.openInterest;
|
|
699
|
+
const dayNtlVlm = update.dayNtlVlm ?? previous?.dayNtlVlm;
|
|
700
|
+
const prevDayPx = update.prevDayPx ?? previous?.prevDayPx;
|
|
701
|
+
const markPx = update.markPx ?? previous?.markPx;
|
|
702
|
+
const midPx = 'midPx' in update ? update.midPx : previous?.midPx;
|
|
703
|
+
const oraclePx = update.oraclePx ?? previous?.oraclePx;
|
|
704
|
+
if (funding === undefined ||
|
|
705
|
+
openInterest === undefined ||
|
|
706
|
+
dayNtlVlm === undefined ||
|
|
707
|
+
prevDayPx === undefined ||
|
|
708
|
+
markPx === undefined ||
|
|
709
|
+
midPx === undefined ||
|
|
710
|
+
oraclePx === undefined) {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
coin: marketId,
|
|
715
|
+
funding,
|
|
716
|
+
openInterest,
|
|
717
|
+
dayNtlVlm,
|
|
718
|
+
prevDayPx,
|
|
719
|
+
markPx,
|
|
720
|
+
midPx,
|
|
721
|
+
oraclePx,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function allDexsAssetCtxEntries(data) {
|
|
725
|
+
return data.assetCtxs ?? data.ctxs ?? [];
|
|
726
|
+
}
|
|
727
|
+
function compactRemovalPrice(levels, value) {
|
|
728
|
+
if (typeof value === 'number') {
|
|
729
|
+
return levels[value]?.price;
|
|
730
|
+
}
|
|
731
|
+
return typeof value === 'string' ? value : value.p;
|
|
732
|
+
}
|
|
733
|
+
function applyCompressedL2Side(previous, updates, removals, side) {
|
|
734
|
+
const byPrice = new Map(previous.map((level) => [level.price, level.size]));
|
|
735
|
+
for (const removal of removals) {
|
|
736
|
+
const price = compactRemovalPrice(previous, removal);
|
|
737
|
+
if (price !== undefined) {
|
|
738
|
+
byPrice.delete(price);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
for (const update of updates) {
|
|
742
|
+
if (Number(update.s) === 0) {
|
|
743
|
+
byPrice.delete(update.p);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
byPrice.set(update.p, update.s);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return [...byPrice.entries()]
|
|
750
|
+
.map(([price, size]) => ({ price, size }))
|
|
751
|
+
.sort((a, b) => side === 'bid'
|
|
752
|
+
? Number(b.price) - Number(a.price)
|
|
753
|
+
: Number(a.price) - Number(b.price))
|
|
754
|
+
.slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE);
|
|
755
|
+
}
|
|
440
756
|
/**
|
|
441
757
|
* Minimal presence/type check of the channel-discriminating and required
|
|
442
758
|
* fields the matching handler dereferences without its own guard. A frame
|
|
@@ -446,7 +762,7 @@ const isObject = (v) => typeof v === 'object' && v !== null;
|
|
|
446
762
|
*/
|
|
447
763
|
function isValidHlFrame(channel, data) {
|
|
448
764
|
// `fastAssetCtxs` carries a base64 string payload, not an object.
|
|
449
|
-
if (channel === 'fastAssetCtxs') {
|
|
765
|
+
if (channel === 'fastAssetCtxs' || channel === 'pac' || channel === 'sac') {
|
|
450
766
|
return typeof data === 'string';
|
|
451
767
|
}
|
|
452
768
|
if (!isObject(data)) {
|
|
@@ -454,15 +770,19 @@ function isValidHlFrame(channel, data) {
|
|
|
454
770
|
}
|
|
455
771
|
switch (channel) {
|
|
456
772
|
case 'allDexsAssetCtxs':
|
|
457
|
-
return Array.isArray(data.assetCtxs);
|
|
773
|
+
return Array.isArray(data.assetCtxs) || Array.isArray(data.ctxs);
|
|
458
774
|
case 'l2Book':
|
|
459
775
|
return (typeof data.coin === 'string' &&
|
|
460
776
|
Array.isArray(data.levels) &&
|
|
461
777
|
Array.isArray(data.levels[0]) &&
|
|
462
778
|
Array.isArray(data.levels[1]) &&
|
|
463
779
|
typeof data.time === 'number');
|
|
780
|
+
case 'l2':
|
|
781
|
+
return typeof data.c === 'string' || isObject(data.s) || isObject(data.u);
|
|
464
782
|
case 'candle':
|
|
465
783
|
return typeof data.s === 'string' && typeof data.i === 'string';
|
|
784
|
+
case 'trades':
|
|
785
|
+
return Array.isArray(data);
|
|
466
786
|
case 'orderUpdates':
|
|
467
787
|
return Array.isArray(data);
|
|
468
788
|
case 'userFills':
|