@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.
Files changed (41) hide show
  1. package/dist/cjs/types/ws.d.ts +48 -1
  2. package/dist/cjs/types/ws.d.ts.map +1 -1
  3. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts +1 -0
  4. package/dist/cjs/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  5. package/dist/cjs/utils/decodeFastAssetCtxs.js +5 -1
  6. package/dist/cjs/utils/decodeFastAssetCtxs.js.map +1 -1
  7. package/dist/cjs/utils/index.d.ts +1 -1
  8. package/dist/cjs/utils/index.d.ts.map +1 -1
  9. package/dist/cjs/utils/index.js +2 -1
  10. package/dist/cjs/utils/index.js.map +1 -1
  11. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +20 -0
  12. package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  13. package/dist/cjs/websocket/HyperliquidWsProvider.js +359 -29
  14. package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
  15. package/dist/esm/types/ws.d.ts +65 -1
  16. package/dist/esm/types/ws.d.ts.map +1 -1
  17. package/dist/esm/utils/decodeFastAssetCtxs.d.ts +1 -0
  18. package/dist/esm/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  19. package/dist/esm/utils/decodeFastAssetCtxs.js +9 -6
  20. package/dist/esm/utils/decodeFastAssetCtxs.js.map +1 -1
  21. package/dist/esm/utils/index.d.ts +1 -1
  22. package/dist/esm/utils/index.d.ts.map +1 -1
  23. package/dist/esm/utils/index.js +1 -1
  24. package/dist/esm/utils/index.js.map +1 -1
  25. package/dist/esm/websocket/HyperliquidWsProvider.d.ts +25 -8
  26. package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  27. package/dist/esm/websocket/HyperliquidWsProvider.js +368 -48
  28. package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
  29. package/dist/types/types/ws.d.ts +65 -1
  30. package/dist/types/types/ws.d.ts.map +1 -1
  31. package/dist/types/utils/decodeFastAssetCtxs.d.ts +1 -0
  32. package/dist/types/utils/decodeFastAssetCtxs.d.ts.map +1 -1
  33. package/dist/types/utils/index.d.ts +1 -1
  34. package/dist/types/utils/index.d.ts.map +1 -1
  35. package/dist/types/websocket/HyperliquidWsProvider.d.ts +25 -8
  36. package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
  37. package/package.json +3 -3
  38. package/src/types/ws.ts +67 -1
  39. package/src/utils/decodeFastAssetCtxs.ts +10 -6
  40. package/src/utils/index.ts +4 -1
  41. 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 { HYPERLIQUID_FEE_TIER_FALLBACK } from '../constants.js';
4
- import { decodeFastAssetCtxs, isOpenAssetPosition, isTriggerOrder, mapFill, mapMarketContext, mapOrderStatus, mapOrderType, mapPosition, priceStepToAggregation, spotAssetFromToken, spotBalance, spotPriceById, } from '../utils/index.js';
5
- /** HL's `l2Book` returns at most this many levels per side (slow/default mode). */
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 the slower
39
- // `allDexsAssetCtxs` context (which supplies oracle/funding/OI/metadata).
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 the all-dexs perp asset-context feed
65
- // (oracle, funding, OI, metadata) with the high-frequency `fastAssetCtxs`
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({ method: 'unsubscribe', subscription: payload }));
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: the all-dexs
115
- * perp asset-context feed (oracle/funding/OI/metadata) plus the compressed
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: 'allDexsAssetCtxs', payload: { type: 'allDexsAssetCtxs' } },
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 `l2Book:${sub.marketId}`;
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: 'l2Book',
159
- coin: sub.marketId,
160
- // HL's l2Book ignores any level-count field; it returns up to 20
161
- // levels/side and controls granularity via nSigFigs (+ mantissa,
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
- for (const [dex, ctxs] of data.assetCtxs) {
239
- const byMarketId = {};
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
- byMarketId[ctx.coin] = ctx;
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
- this.emitMarketsContext();
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: every perp asset context across sub-dexs
270
- * (oracle/funding/OI/metadata), with mid + mark overlaid from the
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 the perp ctx feed and `fastAssetCtxs`. */
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
- this.emit(`l2Book:${data.coin}`, {
321
- channel: 'orderbook',
322
- data: {
323
- provider: this.providerKey,
324
- marketId: data.coin,
325
- bids: data.levels[0]
326
- .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
327
- .map((l) => ({ price: l.px, size: l.sz })),
328
- asks: data.levels[1]
329
- .slice(0, HL_L2_BOOK_MAX_LEVELS_PER_SIDE)
330
- .map((l) => ({ price: l.px, size: l.sz })),
331
- timestamp: data.time,
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':