@lifi/perps-sdk-provider-lighter 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.
Files changed (50) hide show
  1. package/dist/cjs/signers/wasmLoader.d.ts.map +1 -1
  2. package/dist/cjs/signers/wasmLoader.js.map +1 -1
  3. package/dist/cjs/types/ws.d.ts +14 -0
  4. package/dist/cjs/types/ws.d.ts.map +1 -1
  5. package/dist/cjs/utils/index.d.ts +1 -0
  6. package/dist/cjs/utils/index.d.ts.map +1 -1
  7. package/dist/cjs/utils/index.js +3 -1
  8. package/dist/cjs/utils/index.js.map +1 -1
  9. package/dist/cjs/utils/mapMarketContext.d.ts +4 -0
  10. package/dist/cjs/utils/mapMarketContext.d.ts.map +1 -0
  11. package/dist/cjs/utils/mapMarketContext.js +30 -0
  12. package/dist/cjs/utils/mapMarketContext.js.map +1 -0
  13. package/dist/cjs/websocket/LighterWsProvider.d.ts +2 -1
  14. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  15. package/dist/cjs/websocket/LighterWsProvider.js +54 -17
  16. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  17. package/dist/esm/signers/wasmLoader.d.ts.map +1 -1
  18. package/dist/esm/signers/wasmLoader.js +6 -3
  19. package/dist/esm/signers/wasmLoader.js.map +1 -1
  20. package/dist/esm/types/ws.d.ts +23 -3
  21. package/dist/esm/types/ws.d.ts.map +1 -1
  22. package/dist/esm/types/ws.js +2 -2
  23. package/dist/esm/types/ws.js.map +1 -1
  24. package/dist/esm/utils/index.d.ts +1 -0
  25. package/dist/esm/utils/index.d.ts.map +1 -1
  26. package/dist/esm/utils/index.js +1 -0
  27. package/dist/esm/utils/index.js.map +1 -1
  28. package/dist/esm/utils/mapMarketContext.d.ts +11 -0
  29. package/dist/esm/utils/mapMarketContext.d.ts.map +1 -0
  30. package/dist/esm/utils/mapMarketContext.js +33 -0
  31. package/dist/esm/utils/mapMarketContext.js.map +1 -0
  32. package/dist/esm/websocket/LighterWsProvider.d.ts +3 -2
  33. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  34. package/dist/esm/websocket/LighterWsProvider.js +63 -26
  35. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  36. package/dist/types/signers/wasmLoader.d.ts.map +1 -1
  37. package/dist/types/types/ws.d.ts +23 -3
  38. package/dist/types/types/ws.d.ts.map +1 -1
  39. package/dist/types/utils/index.d.ts +1 -0
  40. package/dist/types/utils/index.d.ts.map +1 -1
  41. package/dist/types/utils/mapMarketContext.d.ts +11 -0
  42. package/dist/types/utils/mapMarketContext.d.ts.map +1 -0
  43. package/dist/types/websocket/LighterWsProvider.d.ts +3 -2
  44. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  45. package/package.json +5 -5
  46. package/src/signers/wasmLoader.ts +6 -3
  47. package/src/types/ws.ts +27 -5
  48. package/src/utils/index.ts +1 -0
  49. package/src/utils/mapMarketContext.ts +40 -0
  50. package/src/websocket/LighterWsProvider.ts +77 -26
@@ -0,0 +1,40 @@
1
+ import type { MarketContext } from '@lifi/perps-types'
2
+ import type { LtWsMarketStats, LtWsSpotMarketStats } from '../types/index.js'
3
+
4
+ const isPerpStats = (
5
+ stats: LtWsMarketStats | LtWsSpotMarketStats
6
+ ): stats is LtWsMarketStats => 'mark_price' in stats
7
+
8
+ /**
9
+ * Map a Lighter market-stats record to a {@link MarketContext}: `index_price`
10
+ * is the oracle, `mid_price` the mid. Perp records carry the venue
11
+ * `mark_price`, funding and open interest; spot records have none, so mark
12
+ * falls back to the mid and the perp-only fields stay `undefined`.
13
+ * @public
14
+ */
15
+ export const mapMarketContext = (
16
+ stats: LtWsMarketStats | LtWsSpotMarketStats
17
+ ): MarketContext => {
18
+ const marketId = String(stats.market_id)
19
+ if (isPerpStats(stats)) {
20
+ return {
21
+ marketId,
22
+ midPrice: stats.mid_price,
23
+ markPrice: stats.mark_price,
24
+ oraclePrice: stats.index_price,
25
+ volume24h: stats.daily_quote_token_volume,
26
+ openInterest: stats.open_interest,
27
+ funding: {
28
+ rate: stats.current_funding_rate,
29
+ nextFundingTime: stats.funding_timestamp,
30
+ },
31
+ }
32
+ }
33
+ return {
34
+ marketId,
35
+ midPrice: stats.mid_price,
36
+ markPrice: stats.mid_price,
37
+ oraclePrice: stats.index_price,
38
+ volume24h: String(stats.daily_quote_token_volume),
39
+ }
40
+ }
@@ -12,7 +12,12 @@ import {
12
12
  type WsProviderFactory,
13
13
  wsLog,
14
14
  } from '@lifi/perps-sdk'
15
- import type { Fill, Position, Subscription } from '@lifi/perps-types'
15
+ import type {
16
+ Fill,
17
+ MarketContext,
18
+ Position,
19
+ Subscription,
20
+ } from '@lifi/perps-types'
16
21
  import type { Address } from 'viem'
17
22
  import { LIGHTER_BASE_FEE_TIER, LIGHTER_PROVIDER_KEY } from '../constants.js'
18
23
  import type {
@@ -29,17 +34,19 @@ import type {
29
34
  LtWsOrderBookMessage,
30
35
  LtWsSpotMarketStats,
31
36
  LtWsSpotMarketStatsAllMessage,
37
+ LtWsTradeMessage,
32
38
  } from '../types/index.js'
33
39
  import { LIGHTER_RETRY_DEFAULTS, LighterApiClient } from '../utils/apiClient.js'
34
40
  import {
35
41
  classifyAndMapOrders,
36
42
  fetchDetailedAccount,
37
43
  mapFill,
44
+ mapMarketContext,
38
45
  mapPosition,
39
46
  } from '../utils/index.js'
40
47
 
41
- // Public channels: `prices` (market_stats/all + spot_market_stats/all),
42
- // `orderbook` (order_book/N).
48
+ // Public channels: `marketsContext` (market_stats/all + spot_market_stats/all),
49
+ // `orderbook` (order_book/N), `trades` (trade/N).
43
50
  // Authenticated channels (require an `authProvider` option):
44
51
  // - orderUpdates → account_all_orders/{account_index}
45
52
  // - fills → account_all_trades/{account_index}
@@ -124,7 +131,7 @@ export interface LighterWsProviderOptions {
124
131
 
125
132
  /**
126
133
  * Lighter realtime WS provider (extends {@link WsProviderBase}): subscribes to
127
- * Lighter's WS channels (orderbook, prices, orders, positions), attaching auth
134
+ * Lighter's WS channels (orderbook, marketsContext, orders, positions), attaching auth
128
135
  * tokens to gated channels. Construct via {@link lighterWsProvider}.
129
136
  *
130
137
  * @public
@@ -135,7 +142,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
135
142
  private readonly client: PerpsSDKClient | undefined
136
143
 
137
144
  private readonly orderbooks = new Map<number, OrderbookState>()
138
- private lastPricesByAssetId: Record<string, string> = {}
145
+ private marketsContext: Record<string, MarketContext> = {}
139
146
 
140
147
  /**
141
148
  * `address → market_id → Position`. `subscribed/...` frames reseed it and
@@ -203,9 +210,9 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
203
210
  }
204
211
 
205
212
  // Only the auth channels (orders/fills/positions) resolve markets.
206
- // `prices`/`orderbook` are keyed purely by `String(market_id)`, so gating
207
- // them on the registry sync would let a failed `/markets` fetch kill live
208
- // price ticks.
213
+ // `marketsContext`/`orderbook` are keyed purely by `String(market_id)`, so
214
+ // gating them on the registry sync would let a failed `/markets` fetch kill
215
+ // live price ticks.
209
216
  if (channelNeedsMarkets(sub.channel)) {
210
217
  await this.registry?.sync()
211
218
  }
@@ -214,7 +221,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
214
221
 
215
222
  // Registry keyed by the wire channel (unique per sub), so replay-failure
216
223
  // logs name the channel the venue knows. A logical sub may fan out to
217
- // several wire channels (e.g. `prices`), each registered independently.
224
+ // several wire channels (e.g. `marketsContext`), each registered independently.
218
225
  for (const { channel, needsAuth, address } of wireChannels) {
219
226
  await this.registerSub(channel, { channel, needsAuth, address })
220
227
  }
@@ -239,7 +246,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
239
246
 
240
247
  protected override onClose(): void {
241
248
  this.orderbooks.clear()
242
- this.lastPricesByAssetId = {}
249
+ this.marketsContext = {}
243
250
  this.positionsByAddress.clear()
244
251
  }
245
252
 
@@ -271,10 +278,12 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
271
278
 
272
279
  protected toKey(sub: Subscription): string {
273
280
  switch (sub.channel) {
274
- case 'prices':
275
- return 'prices'
281
+ case 'marketsContext':
282
+ return 'marketsContext'
276
283
  case 'orderbook':
277
284
  return `orderbook:${sub.marketId}`
285
+ case 'trades':
286
+ return `trades:${sub.marketId}`
278
287
  case 'orderUpdates':
279
288
  return `orderUpdates:${sub.address.toLowerCase()}`
280
289
  case 'fills':
@@ -297,10 +306,10 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
297
306
  address?: Address
298
307
  }>
299
308
  > {
300
- if (sub.channel === 'prices') {
309
+ if (sub.channel === 'marketsContext') {
301
310
  // Lighter splits stats across two wire channels: perp markets on
302
311
  // `market_stats/all`, spot markets on `spot_market_stats/all`. Both feed
303
- // the single aggregated `prices` emit.
312
+ // the single aggregated `marketsContext` emit.
304
313
  return [
305
314
  { channel: 'market_stats/all', needsAuth: false },
306
315
  { channel: 'spot_market_stats/all', needsAuth: false },
@@ -316,6 +325,16 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
316
325
  }
317
326
  return [{ channel: `order_book/${id}`, needsAuth: false }]
318
327
  }
328
+ if (sub.channel === 'trades') {
329
+ const id = Number(sub.marketId)
330
+ if (!Number.isFinite(id)) {
331
+ throw new Error(
332
+ `Lighter WS: unknown market for marketId '${sub.marketId}'. ` +
333
+ 'MarketId must be a numeric market_id string.'
334
+ )
335
+ }
336
+ return [{ channel: `trade/${id}`, needsAuth: false }]
337
+ }
319
338
  if (
320
339
  sub.channel === 'orderUpdates' ||
321
340
  sub.channel === 'fills' ||
@@ -423,6 +442,11 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
423
442
  return
424
443
  }
425
444
 
445
+ if (msg.type === 'subscribed/trade' || msg.type === 'update/trade') {
446
+ this.handleTrades(msg as LtWsTradeMessage)
447
+ return
448
+ }
449
+
426
450
  if (
427
451
  msg.type === 'subscribed/account_all_orders' ||
428
452
  msg.type === 'update/account_all_orders'
@@ -554,17 +578,17 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
554
578
  if (!stats) {
555
579
  return
556
580
  }
557
- const updates: Record<string, string> = {}
558
- for (const entry of Object.values(stats)) {
559
- updates[String(entry.market_id)] = entry.last_trade_price
560
- }
561
- if (Object.keys(updates).length === 0) {
581
+ const entries = Object.values(stats)
582
+ if (entries.length === 0) {
562
583
  return
563
584
  }
564
- this.lastPricesByAssetId = { ...this.lastPricesByAssetId, ...updates }
565
- this.emit('prices', {
566
- channel: 'prices',
567
- data: this.lastPricesByAssetId,
585
+ for (const entry of entries) {
586
+ const ctx = mapMarketContext(entry)
587
+ this.marketsContext[ctx.marketId] = ctx
588
+ }
589
+ this.emit('marketsContext', {
590
+ channel: 'marketsContext',
591
+ data: this.marketsContext,
568
592
  })
569
593
  }
570
594
 
@@ -605,11 +629,38 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
605
629
  })
606
630
  }
607
631
 
608
- private marketIdFromChannel(channel: string | undefined): number | null {
609
- if (!channel?.startsWith('order_book')) {
632
+ private handleTrades(msg: LtWsTradeMessage): void {
633
+ const marketId = this.marketIdFromChannel(msg.channel, 'trade')
634
+ if (marketId === null) {
635
+ return
636
+ }
637
+ const trades = msg.trades ?? []
638
+ if (trades.length === 0) {
639
+ return
640
+ }
641
+ const assetId = String(marketId)
642
+ this.emit(`trades:${assetId}`, {
643
+ channel: 'trades',
644
+ data: trades.map((t) => ({
645
+ provider: this.providerKey,
646
+ marketId: assetId,
647
+ price: t.price,
648
+ size: t.size,
649
+ timestamp: t.timestamp,
650
+ side: t.is_maker_ask ? 'buy' : 'sell',
651
+ id: t.trade_id_str ?? String(t.trade_id),
652
+ })),
653
+ })
654
+ }
655
+
656
+ private marketIdFromChannel(
657
+ channel: string | undefined,
658
+ prefix = 'order_book'
659
+ ): number | null {
660
+ if (!channel?.startsWith(prefix)) {
610
661
  return null
611
662
  }
612
- const tail = channel.slice('order_book'.length)
663
+ const tail = channel.slice(prefix.length)
613
664
  if (tail.length < 2 || (tail[0] !== '/' && tail[0] !== ':')) {
614
665
  return null
615
666
  }