@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.
- package/dist/cjs/signers/wasmLoader.d.ts.map +1 -1
- package/dist/cjs/signers/wasmLoader.js.map +1 -1
- package/dist/cjs/types/ws.d.ts +14 -0
- package/dist/cjs/types/ws.d.ts.map +1 -1
- package/dist/cjs/utils/index.d.ts +1 -0
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +3 -1
- 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 +30 -0
- package/dist/cjs/utils/mapMarketContext.js.map +1 -0
- package/dist/cjs/websocket/LighterWsProvider.d.ts +2 -1
- package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/LighterWsProvider.js +54 -17
- package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
- package/dist/esm/signers/wasmLoader.d.ts.map +1 -1
- package/dist/esm/signers/wasmLoader.js +6 -3
- package/dist/esm/signers/wasmLoader.js.map +1 -1
- package/dist/esm/types/ws.d.ts +23 -3
- package/dist/esm/types/ws.d.ts.map +1 -1
- package/dist/esm/types/ws.js +2 -2
- package/dist/esm/types/ws.js.map +1 -1
- package/dist/esm/utils/index.d.ts +1 -0
- package/dist/esm/utils/index.d.ts.map +1 -1
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/mapMarketContext.d.ts +11 -0
- package/dist/esm/utils/mapMarketContext.d.ts.map +1 -0
- package/dist/esm/utils/mapMarketContext.js +33 -0
- package/dist/esm/utils/mapMarketContext.js.map +1 -0
- package/dist/esm/websocket/LighterWsProvider.d.ts +3 -2
- package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/LighterWsProvider.js +63 -26
- package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
- package/dist/types/signers/wasmLoader.d.ts.map +1 -1
- package/dist/types/types/ws.d.ts +23 -3
- package/dist/types/types/ws.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/mapMarketContext.d.ts +11 -0
- package/dist/types/utils/mapMarketContext.d.ts.map +1 -0
- package/dist/types/websocket/LighterWsProvider.d.ts +3 -2
- package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/signers/wasmLoader.ts +6 -3
- package/src/types/ws.ts +27 -5
- package/src/utils/index.ts +1 -0
- package/src/utils/mapMarketContext.ts +40 -0
- 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 {
|
|
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: `
|
|
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,
|
|
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
|
|
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
|
-
// `
|
|
207
|
-
// them on the registry sync would let a failed `/markets` fetch kill
|
|
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. `
|
|
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.
|
|
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 '
|
|
275
|
-
return '
|
|
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 === '
|
|
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 `
|
|
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
|
|
558
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
609
|
-
|
|
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(
|
|
663
|
+
const tail = channel.slice(prefix.length)
|
|
613
664
|
if (tail.length < 2 || (tail[0] !== '/' && tail[0] !== ':')) {
|
|
614
665
|
return null
|
|
615
666
|
}
|