@lifi/perps-sdk-provider-lighter 24.0.2 → 26.0.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 (117) hide show
  1. package/README.md +55 -0
  2. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  3. package/dist/cjs/LighterProvider.js +71 -56
  4. package/dist/cjs/LighterProvider.js.map +1 -1
  5. package/dist/cjs/accountConfig.d.ts.map +1 -1
  6. package/dist/cjs/accountConfig.js +1 -7
  7. package/dist/cjs/accountConfig.js.map +1 -1
  8. package/dist/cjs/constants.d.ts +2 -0
  9. package/dist/cjs/constants.d.ts.map +1 -1
  10. package/dist/cjs/constants.js +3 -0
  11. package/dist/cjs/constants.js.map +1 -1
  12. package/dist/cjs/signers/LighterSigner.d.ts.map +1 -1
  13. package/dist/cjs/signers/LighterSigner.js +2 -1
  14. package/dist/cjs/signers/LighterSigner.js.map +1 -1
  15. package/dist/cjs/types/submit.d.ts.map +1 -1
  16. package/dist/cjs/utils/activityCursor.d.ts.map +1 -1
  17. package/dist/cjs/utils/activityCursor.js +29 -6
  18. package/dist/cjs/utils/activityCursor.js.map +1 -1
  19. package/dist/cjs/utils/index.d.ts +1 -1
  20. package/dist/cjs/utils/index.d.ts.map +1 -1
  21. package/dist/cjs/utils/index.js +2 -7
  22. package/dist/cjs/utils/index.js.map +1 -1
  23. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  24. package/dist/cjs/utils/mapFill.js +4 -2
  25. package/dist/cjs/utils/mapFill.js.map +1 -1
  26. package/dist/cjs/utils/mapOrder.d.ts +4 -11
  27. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  28. package/dist/cjs/utils/mapOrder.js +112 -120
  29. package/dist/cjs/utils/mapOrder.js.map +1 -1
  30. package/dist/cjs/utils/txHash.d.ts +2 -0
  31. package/dist/cjs/utils/txHash.d.ts.map +1 -0
  32. package/dist/cjs/utils/txHash.js +10 -0
  33. package/dist/cjs/utils/txHash.js.map +1 -0
  34. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  35. package/dist/cjs/websocket/LighterWsProvider.js +11 -9
  36. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  37. package/dist/esm/LighterProvider.d.ts.map +1 -1
  38. package/dist/esm/LighterProvider.js +78 -65
  39. package/dist/esm/LighterProvider.js.map +1 -1
  40. package/dist/esm/accountConfig.d.ts.map +1 -1
  41. package/dist/esm/accountConfig.js +5 -16
  42. package/dist/esm/accountConfig.js.map +1 -1
  43. package/dist/esm/constants.d.ts +9 -3
  44. package/dist/esm/constants.d.ts.map +1 -1
  45. package/dist/esm/constants.js +6 -3
  46. package/dist/esm/constants.js.map +1 -1
  47. package/dist/esm/signers/LighterSigner.d.ts.map +1 -1
  48. package/dist/esm/signers/LighterSigner.js +2 -1
  49. package/dist/esm/signers/LighterSigner.js.map +1 -1
  50. package/dist/esm/types/submit.d.ts +16 -6
  51. package/dist/esm/types/submit.d.ts.map +1 -1
  52. package/dist/esm/types/ws.d.ts +1 -1
  53. package/dist/esm/utils/activityCursor.d.ts +2 -11
  54. package/dist/esm/utils/activityCursor.d.ts.map +1 -1
  55. package/dist/esm/utils/activityCursor.js +35 -7
  56. package/dist/esm/utils/activityCursor.js.map +1 -1
  57. package/dist/esm/utils/index.d.ts +1 -1
  58. package/dist/esm/utils/index.d.ts.map +1 -1
  59. package/dist/esm/utils/index.js +1 -1
  60. package/dist/esm/utils/index.js.map +1 -1
  61. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  62. package/dist/esm/utils/mapFill.js +5 -3
  63. package/dist/esm/utils/mapFill.js.map +1 -1
  64. package/dist/esm/utils/mapOrder.d.ts +6 -68
  65. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  66. package/dist/esm/utils/mapOrder.js +111 -180
  67. package/dist/esm/utils/mapOrder.js.map +1 -1
  68. package/dist/esm/utils/txHash.d.ts +8 -0
  69. package/dist/esm/utils/txHash.d.ts.map +1 -0
  70. package/dist/esm/utils/txHash.js +18 -0
  71. package/dist/esm/utils/txHash.js.map +1 -0
  72. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  73. package/dist/esm/websocket/LighterWsProvider.js +20 -16
  74. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  75. package/dist/types/LighterProvider.d.ts.map +1 -1
  76. package/dist/types/accountConfig.d.ts.map +1 -1
  77. package/dist/types/constants.d.ts +9 -3
  78. package/dist/types/constants.d.ts.map +1 -1
  79. package/dist/types/signers/LighterSigner.d.ts.map +1 -1
  80. package/dist/types/types/submit.d.ts +16 -6
  81. package/dist/types/types/submit.d.ts.map +1 -1
  82. package/dist/types/types/ws.d.ts +1 -1
  83. package/dist/types/utils/activityCursor.d.ts +2 -11
  84. package/dist/types/utils/activityCursor.d.ts.map +1 -1
  85. package/dist/types/utils/index.d.ts +1 -1
  86. package/dist/types/utils/index.d.ts.map +1 -1
  87. package/dist/types/utils/mapFill.d.ts.map +1 -1
  88. package/dist/types/utils/mapOrder.d.ts +6 -68
  89. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  90. package/dist/types/utils/txHash.d.ts +8 -0
  91. package/dist/types/utils/txHash.d.ts.map +1 -0
  92. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  93. package/dist/wasm/lighter-signer.wasm +0 -0
  94. package/package.json +3 -3
  95. package/src/LighterProvider.ts +87 -98
  96. package/src/accountConfig.ts +5 -18
  97. package/src/constants.ts +11 -3
  98. package/src/signers/LighterSigner.ts +2 -0
  99. package/src/types/submit.ts +16 -6
  100. package/src/types/ws.ts +1 -1
  101. package/src/utils/activityCursor.ts +50 -18
  102. package/src/utils/index.ts +1 -9
  103. package/src/utils/mapFill.ts +5 -8
  104. package/src/utils/mapOrder.ts +130 -207
  105. package/src/utils/txHash.ts +20 -0
  106. package/src/websocket/LighterWsProvider.ts +22 -17
  107. package/dist/cjs/utils/mapTwap.d.ts +0 -4
  108. package/dist/cjs/utils/mapTwap.d.ts.map +0 -1
  109. package/dist/cjs/utils/mapTwap.js +0 -31
  110. package/dist/cjs/utils/mapTwap.js.map +0 -1
  111. package/dist/esm/utils/mapTwap.d.ts +0 -5
  112. package/dist/esm/utils/mapTwap.d.ts.map +0 -1
  113. package/dist/esm/utils/mapTwap.js +0 -25
  114. package/dist/esm/utils/mapTwap.js.map +0 -1
  115. package/dist/types/utils/mapTwap.d.ts +0 -5
  116. package/dist/types/utils/mapTwap.d.ts.map +0 -1
  117. package/src/utils/mapTwap.ts +0 -31
@@ -1,35 +1,46 @@
1
- import { isActiveOrderStatus } from '@lifi/perps-sdk'
2
- import type {
3
- MarketDisplay,
4
- OpenOrder,
5
- Order,
6
- TriggerOrder,
7
- } from '@lifi/perps-types'
1
+ import {
2
+ isActiveOrderStatus,
3
+ PerpsError,
4
+ triggerConditionFor,
5
+ } from '@lifi/perps-sdk'
6
+ import type { MarketDisplay, Order, OrderBase } from '@lifi/perps-types'
8
7
  import {
9
8
  OrderSide,
10
9
  OrderStatus,
11
10
  OrderType,
11
+ PerpsErrorCode,
12
12
  TimeInForce,
13
13
  } from '@lifi/perps-types'
14
+ import Big from 'big.js'
14
15
  import type { LtOrder } from '../types/index.js'
15
16
 
16
- // Lighter's `type` enum uses hyphens in the OpenAPI spec but earlier API
17
- // versions emitted underscores. Tolerate both so we don't silently fall
18
- // through to LIMIT for stop/take-profit orders.
19
- const mapOrderType = (ltType: string): OrderType => {
20
- const normalized = ltType.replace(/-/g, '_')
21
- const map: Record<string, OrderType> = {
22
- limit: OrderType.LIMIT,
23
- market: OrderType.MARKET,
24
- stop_loss: OrderType.STOP_MARKET,
25
- stop_loss_limit: OrderType.STOP_LIMIT,
26
- take_profit: OrderType.TAKE_PROFIT_MARKET,
27
- take_profit_limit: OrderType.TAKE_PROFIT_LIMIT,
17
+ const mapOrderType = (type: string): Order['type'] => {
18
+ switch (type.replace(/-/g, '_')) {
19
+ case 'market':
20
+ case 'liquidation':
21
+ return OrderType.MARKET
22
+ case 'limit':
23
+ case 'twap_sub':
24
+ return OrderType.LIMIT
25
+ case 'stop_loss':
26
+ return OrderType.STOP_MARKET
27
+ case 'stop_loss_limit':
28
+ return OrderType.STOP_LIMIT
29
+ case 'take_profit':
30
+ return OrderType.TAKE_PROFIT_MARKET
31
+ case 'take_profit_limit':
32
+ return OrderType.TAKE_PROFIT_LIMIT
33
+ case 'twap':
34
+ return OrderType.TWAP
35
+ default:
36
+ throw new PerpsError(
37
+ PerpsErrorCode.SDKError,
38
+ `Unknown Lighter order type: ${type}`
39
+ )
28
40
  }
29
- return map[normalized] ?? OrderType.LIMIT
30
41
  }
31
42
 
32
- const mapTimeInForce = (tif: string): TimeInForce | undefined => {
43
+ const mapTimeInForce = (tif: string): TimeInForce => {
33
44
  switch (tif.replace(/-/g, '_')) {
34
45
  case 'good_till_time':
35
46
  return TimeInForce.GTT
@@ -38,222 +49,134 @@ const mapTimeInForce = (tif: string): TimeInForce | undefined => {
38
49
  case 'post_only':
39
50
  return TimeInForce.POST_ONLY
40
51
  default:
41
- return undefined
42
- }
43
- }
44
-
45
- const mapOrderStatus = (status: string): OrderStatus => {
46
- if (
47
- status === 'open' ||
48
- status === 'in-progress' ||
49
- status === 'in_progress'
50
- ) {
51
- return OrderStatus.OPEN
52
+ throw new PerpsError(
53
+ PerpsErrorCode.SDKError,
54
+ `Unknown Lighter time in force: ${tif}`
55
+ )
52
56
  }
53
- if (status === 'pending') {
54
- return OrderStatus.PENDING
55
- }
56
- if (status === 'filled') {
57
- return OrderStatus.FILLED
58
- }
59
- if (status.startsWith('canceled')) {
60
- return OrderStatus.CANCELLED
61
- }
62
- return OrderStatus.OPEN
63
57
  }
64
58
 
65
- /**
66
- * Map a raw Lighter order status to a short English sentence describing
67
- * *why* the order ended in a terminal non-FILLED state. Non-terminal
68
- * statuses, plain `filled`, and unknown values return `undefined`.
69
- * @public
70
- */
71
- export const mapStatusReason = (status: string): string | undefined => {
72
- switch (status) {
59
+ const mapOrderStatus = (
60
+ order: LtOrder,
61
+ filledSize = new Big(order.filled_base_amount)
62
+ ): OrderStatus => {
63
+ switch (order.status) {
64
+ case 'pending':
65
+ case 'in-progress':
66
+ return OrderStatus.ACCEPTED
67
+ case 'open':
68
+ if (order.trigger_status === 'parent-order') {
69
+ return OrderStatus.PENDING
70
+ }
71
+ return filledSize.gt(0) ? OrderStatus.PARTIALLY_FILLED : OrderStatus.OPEN
72
+ case 'triggered':
73
+ return OrderStatus.TRIGGERED
74
+ case 'filled':
75
+ return OrderStatus.FILLED
76
+ case 'canceled-expired':
77
+ return OrderStatus.EXPIRED
73
78
  case 'canceled':
74
- return 'Order cancelled.'
75
79
  case 'canceled-post-only':
76
- return 'Order cancelled: post-only order would have crossed the book.'
77
80
  case 'canceled-reduce-only':
78
- return 'Order cancelled: would not reduce your position.'
79
81
  case 'canceled-position-not-allowed':
80
- return 'Order cancelled: position not allowed.'
81
82
  case 'canceled-margin-not-allowed':
82
- return 'Order cancelled: insufficient margin.'
83
83
  case 'canceled-too-much-slippage':
84
- return 'Order cancelled: slippage exceeded tolerance.'
85
84
  case 'canceled-not-enough-liquidity':
86
- return 'Order cancelled: not enough liquidity to fill.'
87
85
  case 'canceled-self-trade':
88
- return 'Order cancelled: would self-trade against your own resting order.'
89
- case 'canceled-expired':
90
- return 'Order expired.'
91
86
  case 'canceled-oco':
92
- return 'Order cancelled: sibling OCO order filled or cancelled first.'
93
87
  case 'canceled-child':
94
- return 'Order cancelled: parent order was cancelled.'
95
88
  case 'canceled-liquidation':
96
- return 'Order cancelled: account was liquidated.'
97
89
  case 'canceled-invalid-balance':
98
- return 'Order cancelled: invalid balance.'
90
+ return OrderStatus.CANCELLED
99
91
  default:
100
- return undefined
92
+ throw new PerpsError(
93
+ PerpsErrorCode.SDKError,
94
+ `Unknown Lighter order status: ${order.status}`
95
+ )
101
96
  }
102
97
  }
103
98
 
104
- /**
105
- * True for order types Lighter exposes as TP/SL legs. Mirrors the
106
- * Hyperliquid helper of the same name so the backend can split a raw
107
- * Lighter order list into the same `openOrders` / `triggerOrders`
108
- * buckets the SDK declares on `OrdersResponse`.
109
- * @public
110
- */
111
- export const isTriggerType = (type: OrderType): boolean =>
112
- type === OrderType.TAKE_PROFIT_MARKET ||
113
- type === OrderType.TAKE_PROFIT_LIMIT ||
114
- type === OrderType.STOP_MARKET ||
115
- type === OrderType.STOP_LIMIT
116
-
117
- /**
118
- * Decide whether a raw Lighter order belongs in the cross-provider
119
- * `triggerOrders` bucket. Single source of truth for both the REST
120
- * `getOrders` path and the `account_all_orders` WS handler.
121
- *
122
- * Authoritative signals, in order:
123
- * 1. `trigger_status` — Lighter's own enum. `'ready' | 'mark-price' |
124
- * 'parent-order'` all mean "this order has trigger semantics" (TP/SL
125
- * waiting on price, or a TP/SL leg of a parent placeOrder). `'twap'`
126
- * is intentionally excluded — TWAP child orders are scheduled resting
127
- * limits, not TP/SL triggers. `'na'` is "regular order".
128
- * 2. `trigger_price > 0` — catches cases where `trigger_status`
129
- * momentarily lags during state transitions (e.g. right after an OCO
130
- * sibling cancels).
131
- * 3. `type` — final fallback via the mapped `OrderType`.
132
- * @public
133
- */
134
- export const isTriggerOrder = (raw: LtOrder): boolean => {
135
- if (
136
- raw.trigger_status === 'ready' ||
137
- raw.trigger_status === 'mark-price' ||
138
- raw.trigger_status === 'parent-order'
139
- ) {
140
- return true
141
- }
142
- if (raw.trigger_price !== '' && parseFloat(raw.trigger_price) > 0) {
143
- return true
144
- }
145
- return isTriggerType(mapOrderType(raw.type))
146
- }
147
-
148
- /**
149
- * Map a raw Lighter trigger order (stop/take-profit, market or limit) to
150
- * the generic `TriggerOrder` shape. For market variants the `limitPrice`
151
- * field is omitted; for limit variants `order.price` is the limit and
152
- * `order.trigger_price` is the activation level.
153
- * @public
154
- */
155
- export const mapTriggerOrder = (
156
- order: LtOrder,
157
- market: MarketDisplay
158
- ): TriggerOrder => {
99
+ /** Map a Lighter order with its venue identity, lifecycle, and execution fields. */
100
+ export const mapOrder = (order: LtOrder, market: MarketDisplay): Order => {
159
101
  const type = mapOrderType(order.type)
160
- const isLimit =
161
- type === OrderType.TAKE_PROFIT_LIMIT || type === OrderType.STOP_LIMIT
162
- return {
163
- // `order_index` is the per-(account, market) numeric id Lighter's L2
164
- // mutating txs (cancel, modify) require. The matching-engine string
165
- // label `order_id` is internal to the SDK provider and never crosses
166
- // this boundary — combined with `market.id` (=market_index), the
167
- // numeric `order_index` uniquely pins a Lighter order.
102
+ const filledSize = new Big(order.filled_base_amount)
103
+ const status = mapOrderStatus(order, filledSize)
104
+ const base: OrderBase = {
168
105
  orderId: String(order.order_index),
106
+ ...(order.client_order_index === 0
107
+ ? {}
108
+ : { clientOrderId: String(order.client_order_index) }),
169
109
  market,
170
- type,
171
- size: order.initial_base_amount,
172
- triggerPrice: order.trigger_price,
173
- ...(isLimit ? { limitPrice: order.price } : {}),
110
+ side: order.is_ask ? OrderSide.SELL : OrderSide.BUY,
111
+ status,
112
+ ...(status === OrderStatus.CANCELLED ? { statusReason: order.status } : {}),
113
+ originalSize: order.initial_base_amount,
114
+ remainingSize: order.remaining_base_amount,
115
+ filledSize: order.filled_base_amount,
116
+ ...(filledSize.gt(0)
117
+ ? {
118
+ averagePrice: new Big(order.filled_quote_amount)
119
+ .div(filledSize)
120
+ .toString(),
121
+ }
122
+ : {}),
123
+ reduceOnly: order.reduce_only,
124
+ ...(order.parent_order_id ? { parentOrderId: order.parent_order_id } : {}),
174
125
  createdAt: new Date(order.created_at * 1000).toISOString(),
126
+ updatedAt: new Date(order.updated_at * 1000).toISOString(),
127
+ }
128
+ switch (type) {
129
+ case OrderType.TWAP:
130
+ return {
131
+ ...base,
132
+ type,
133
+ durationSeconds: order.order_expiry / 1000 - order.created_at,
134
+ startedAt: base.createdAt,
135
+ }
136
+ case OrderType.STOP_MARKET:
137
+ case OrderType.STOP_LIMIT:
138
+ case OrderType.TAKE_PROFIT_MARKET:
139
+ case OrderType.TAKE_PROFIT_LIMIT:
140
+ return {
141
+ ...base,
142
+ type,
143
+ triggerPrice: order.trigger_price,
144
+ triggerCondition: triggerConditionFor(type, base.side),
145
+ ...(type === OrderType.STOP_LIMIT ||
146
+ type === OrderType.TAKE_PROFIT_LIMIT
147
+ ? { limitPrice: order.price }
148
+ : {}),
149
+ }
150
+ case OrderType.MARKET:
151
+ case OrderType.LIMIT:
152
+ return {
153
+ ...base,
154
+ type,
155
+ price: order.price,
156
+ timeInForce: mapTimeInForce(order.time_in_force),
157
+ ...(order.order_expiry > 0
158
+ ? { expiresAt: new Date(order.order_expiry).toISOString() }
159
+ : {}),
160
+ }
175
161
  }
176
162
  }
177
163
 
178
- /**
179
- * Map a raw Lighter order to the generic OpenOrder type.
180
- * @param market - Backend-resolved market identity for `order.market_index`.
181
- * @public
182
- */
183
- export const mapOrder = (order: LtOrder, market: MarketDisplay): OpenOrder => ({
184
- orderId: String(order.order_index),
185
- market,
186
- side: order.is_ask ? OrderSide.SELL : OrderSide.BUY,
187
- type: mapOrderType(order.type),
188
- originalSize: order.initial_base_amount,
189
- remainingSize: order.remaining_base_amount,
190
- price: order.price,
191
- filledSize: order.filled_base_amount,
192
- reduceOnly: order.reduce_only,
193
- createdAt: new Date(order.created_at * 1000).toISOString(),
194
- })
195
-
196
- /**
197
- * Walk a flat list of raw Lighter orders and bucket each into the
198
- * `OpenOrder` / `TriggerOrder` arrays the cross-provider `OrdersResponse`
199
- * declares, plus a `terminated` list of orderIds whose status has reached
200
- * a terminal state (filled / cancelled / rejected / expired) — consumers
201
- * applying WS deltas evict those from the cached buckets. REST callers
202
- * see `terminated: []` because `accountActiveOrders` returns active only.
203
- * An order whose market the resolver does not know is skipped.
204
- * @public
205
- */
206
- export const classifyAndMapOrders = (
207
- orders: LtOrder[],
164
+ /** Include terminal rows and their ids so consumers can update history and active caches. */
165
+ export const mapOrderUpdates = (
166
+ rawOrders: LtOrder[],
208
167
  resolveMarket: (marketIndex: number) => MarketDisplay | undefined
209
- ): {
210
- openOrders: OpenOrder[]
211
- triggerOrders: TriggerOrder[]
212
- terminated: string[]
213
- } => {
214
- const openOrders: OpenOrder[] = []
215
- const triggerOrders: TriggerOrder[] = []
168
+ ): { orders: Order[]; terminated: string[] } => {
169
+ const orders: Order[] = []
216
170
  const terminated: string[] = []
217
- for (const raw of orders) {
218
- const orderId = String(raw.order_index)
219
- if (!isActiveOrderStatus(mapOrderStatus(raw.status))) {
220
- terminated.push(orderId)
221
- continue
222
- }
171
+ for (const raw of rawOrders) {
223
172
  const market = resolveMarket(raw.market_index)
224
- if (market === undefined) {
225
- continue
173
+ const order = market === undefined ? undefined : mapOrder(raw, market)
174
+ if (!isActiveOrderStatus(order?.status ?? mapOrderStatus(raw))) {
175
+ terminated.push(String(raw.order_index))
226
176
  }
227
- if (isTriggerOrder(raw)) {
228
- triggerOrders.push(mapTriggerOrder(raw, market))
229
- } else {
230
- openOrders.push(mapOrder(raw, market))
177
+ if (order !== undefined) {
178
+ orders.push(order)
231
179
  }
232
180
  }
233
- return { openOrders, triggerOrders, terminated }
181
+ return { orders, terminated }
234
182
  }
235
-
236
- /**
237
- * Map a raw Lighter order to the rich Order type — adds status, time-in-force
238
- * and remaining/filled sizes on top of the OpenOrder fields.
239
- * @public
240
- */
241
- export const mapOrderDetail = (
242
- order: LtOrder,
243
- market: MarketDisplay
244
- ): Order => ({
245
- orderId: String(order.order_index),
246
- market,
247
- side: order.is_ask ? OrderSide.SELL : OrderSide.BUY,
248
- type: mapOrderType(order.type),
249
- price: order.price,
250
- originalSize: order.initial_base_amount,
251
- remainingSize: order.remaining_base_amount,
252
- filledSize: order.filled_base_amount,
253
- timeInForce: mapTimeInForce(order.time_in_force),
254
- reduceOnly: order.reduce_only,
255
- status: mapOrderStatus(order.status),
256
- statusReason: mapStatusReason(order.status),
257
- createdAt: new Date(order.created_at * 1000).toISOString(),
258
- updatedAt: new Date(order.updated_at * 1000).toISOString(),
259
- })
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Trailing hex characters a Lighter placeholder `tx_hash` leaves zero. Lighter
3
+ * reports a 40-byte hash on every fill and transfer row, and a row that settled
4
+ * no transaction carries a sequence pair in the leading 16 bytes and zero in
5
+ * the remaining 24. A settled row fills all 40 bytes.
6
+ */
7
+ const PLACEHOLDER_ZERO_SUFFIX_LENGTH = 48
8
+
9
+ const ALL_ZERO = /^0+$/
10
+
11
+ /**
12
+ * Report whether a Lighter `tx_hash` is the placeholder the venue sends for a
13
+ * row that settled no transaction. Such a row resolves to no explorer link.
14
+ *
15
+ * @internal
16
+ */
17
+ export const isPlaceholderTxHash = (txHash: string | undefined): boolean =>
18
+ txHash !== undefined &&
19
+ txHash.length > PLACEHOLDER_ZERO_SUFFIX_LENGTH &&
20
+ ALL_ZERO.test(txHash.slice(-PLACEHOLDER_ZERO_SUFFIX_LENGTH))
@@ -29,6 +29,7 @@ import {
29
29
  DEFAULT_LIGHTER_WS_URL,
30
30
  LIGHTER_BASE_FEE_TIER,
31
31
  LIGHTER_PROVIDER_KEY,
32
+ LIGHTER_SPOT_CATEGORY_ID,
32
33
  } from '../constants.js'
33
34
  import type { LighterPerpsProvider } from '../LighterProvider.js'
34
35
  import type {
@@ -50,10 +51,10 @@ import type {
50
51
  } from '../types/index.js'
51
52
  import { LIGHTER_RETRY_DEFAULTS, LighterApiClient } from '../utils/apiClient.js'
52
53
  import {
53
- classifyAndMapOrders,
54
54
  fetchDetailedAccount,
55
55
  mapFill,
56
56
  mapMarketContext,
57
+ mapOrderUpdates,
57
58
  mapPosition,
58
59
  toRequiredBig,
59
60
  } from '../utils/index.js'
@@ -84,18 +85,19 @@ import {
84
85
  // Orderbook is stateful: the first message is a full snapshot, subsequent
85
86
  // messages are deltas where size=0 deletes a level.
86
87
 
87
- const LIGHTER_SPOT_MARKET_ID_OFFSET = 2048
88
-
89
88
  const LIGHTER_AUTH_CHANNEL = {
90
89
  orderUpdates: 'account_all_orders',
91
90
  fills: 'account_all_trades',
92
91
  positions: 'account_all_positions',
93
92
  } as const
94
93
 
95
- /** Channels whose handlers resolve market identity from the registry. */
94
+ /** Channels whose wire name or handler reads market identity from the registry. */
96
95
  function channelNeedsMarkets(channel: Subscription['channel']): boolean {
97
96
  return (
98
- channel === 'orderUpdates' || channel === 'fills' || channel === 'positions'
97
+ channel === 'marketContext' ||
98
+ channel === 'orderUpdates' ||
99
+ channel === 'fills' ||
100
+ channel === 'positions'
99
101
  )
100
102
  }
101
103
 
@@ -258,10 +260,10 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
258
260
  }
259
261
 
260
262
  protected async openChannel(sub: Subscription): Promise<() => void> {
261
- // Only the auth channels (orders/fills/positions) resolve markets.
262
- // `marketsContext`/`orderbook` are keyed purely by `String(market_id)`, so
263
- // gating them on the registry sync would let a failed `/markets` fetch kill
264
- // live price ticks.
263
+ // `marketsContext` resolves to two fixed `/all` channels and `orderbook` is
264
+ // keyed purely by `String(market_id)`, so neither needs the registry;
265
+ // gating them on the sync would let a failed `/markets` fetch kill live
266
+ // price ticks.
265
267
  if (channelNeedsMarkets(sub.channel)) {
266
268
  await this.registry?.sync()
267
269
  }
@@ -393,18 +395,21 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
393
395
  ]
394
396
  }
395
397
  if (sub.channel === 'marketContext') {
396
- const id = Number(sub.marketId)
397
- if (!Number.isFinite(id)) {
398
- throw new Error(
399
- `Lighter WS: unknown market for marketId '${sub.marketId}'. ` +
400
- 'MarketId must be a numeric market_id string.'
398
+ // Lighter market ids carry no type information, so the wire channel comes
399
+ // from the registry row's category, not from the numeric id.
400
+ if (this.registry === undefined) {
401
+ throw new PerpsError(
402
+ PerpsErrorCode.MarketNotFound,
403
+ `Lighter WS: no market registry for marketId '${sub.marketId}'. ` +
404
+ 'Construct via `lighterWsProvider({...})` and register with PerpsWsClient.'
401
405
  )
402
406
  }
407
+ const market = this.registry.require(sub.marketId)
403
408
  const prefix =
404
- id >= LIGHTER_SPOT_MARKET_ID_OFFSET
409
+ market.categoryId === LIGHTER_SPOT_CATEGORY_ID
405
410
  ? 'spot_market_stats'
406
411
  : 'market_stats'
407
- return [{ channel: `${prefix}/${id}`, needsAuth: false }]
412
+ return [{ channel: `${prefix}/${market.id}`, needsAuth: false }]
408
413
  }
409
414
  if (sub.channel === 'orderbook') {
410
415
  const id = Number(sub.marketId)
@@ -598,7 +603,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
598
603
  }
599
604
  const raw = collectAuthChannelItems<LtOrder>(msg, 'orders')
600
605
  // Unknown market id (absent from the synced snapshot); the order is skipped.
601
- const data = classifyAndMapOrders(raw, (marketIndex) =>
606
+ const data = mapOrderUpdates(raw, (marketIndex) =>
602
607
  this.registry?.get(String(marketIndex))
603
608
  )
604
609
  this.emit(`orderUpdates:${address}`, {
@@ -1,4 +0,0 @@
1
- import { type Market, type TwapOrder } from '@lifi/perps-types';
2
- import type { LtOrder } from '../types/order.js';
3
- export declare const mapRunningTwap: (order: LtOrder, market: Market) => TwapOrder;
4
- //# sourceMappingURL=mapTwap.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapTwap.d.ts","sourceRoot":"","sources":["../../../src/utils/mapTwap.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAA;AAE1B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAGhD,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,EAAE,QAAQ,MAAM,KAAG,SAmB/D,CAAA"}
@@ -1,31 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.mapRunningTwap = void 0;
7
- const perps_sdk_1 = require("@lifi/perps-sdk");
8
- const perps_types_1 = require("@lifi/perps-types");
9
- const big_js_1 = __importDefault(require("big.js"));
10
- const mapRunningTwap = (order, market) => {
11
- const filledSize = new big_js_1.default(order.filled_base_amount);
12
- return {
13
- twapId: String(order.order_index),
14
- market: (0, perps_sdk_1.toMarketDisplay)(market),
15
- side: order.is_ask ? perps_types_1.OrderSide.SELL : perps_types_1.OrderSide.BUY,
16
- totalSize: order.initial_base_amount,
17
- filledSize: order.filled_base_amount,
18
- ...(filledSize.eq(0)
19
- ? {}
20
- : {
21
- avgFillPrice: new big_js_1.default(order.filled_quote_amount)
22
- .div(filledSize)
23
- .toString(),
24
- }),
25
- startedAt: new Date(order.created_at * 1000).toISOString(),
26
- durationSeconds: (order.order_expiry - order.created_at * 1000) / 1000,
27
- status: perps_types_1.TwapOrderStatus.RUNNING,
28
- };
29
- };
30
- exports.mapRunningTwap = mapRunningTwap;
31
- //# sourceMappingURL=mapTwap.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapTwap.js","sourceRoot":"","sources":["../../../src/utils/mapTwap.ts"],"names":[],"mappings":";;;;;;AAAA,+CAAiD;AACjD,mDAK0B;AAC1B,oDAAwB;AAIjB,MAAM,cAAc,GAAG,CAAC,KAAc,EAAE,MAAc,EAAa,EAAE;IAC1E,MAAM,UAAU,GAAG,IAAI,gBAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACpD,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC;QACjC,MAAM,EAAE,IAAA,2BAAe,EAAC,MAAM,CAAC;QAC/B,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,uBAAS,CAAC,IAAI,CAAC,CAAC,CAAC,uBAAS,CAAC,GAAG;QACnD,SAAS,EAAE,KAAK,CAAC,mBAAmB;QACpC,UAAU,EAAE,KAAK,CAAC,kBAAkB;QACpC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;YAClB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE,YAAY,EAAE,IAAI,gBAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC;qBAC7C,GAAG,CAAC,UAAU,CAAC;qBACf,QAAQ,EAAE;aACd,CAAC;QACN,SAAS,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;QAC1D,eAAe,EAAE,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,IAAI;QACtE,MAAM,EAAE,6BAAe,CAAC,OAAO;KAChC,CAAA;AACH,CAAC,CAAA;AAnBY,QAAA,cAAc,kBAmB1B"}
@@ -1,5 +0,0 @@
1
- import { type Market, type TwapOrder } from '@lifi/perps-types';
2
- import type { LtOrder } from '../types/order.js';
3
- /** Map an active Lighter `twap` parent order to the shared running-TWAP model. */
4
- export declare const mapRunningTwap: (order: LtOrder, market: Market) => TwapOrder;
5
- //# sourceMappingURL=mapTwap.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapTwap.d.ts","sourceRoot":"","sources":["../../../src/utils/mapTwap.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAA;AAE1B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAEhD,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,EAAE,QAAQ,MAAM,KAAG,SAmB/D,CAAA"}
@@ -1,25 +0,0 @@
1
- import { toMarketDisplay } from '@lifi/perps-sdk';
2
- import { OrderSide, TwapOrderStatus, } from '@lifi/perps-types';
3
- import Big from 'big.js';
4
- /** Map an active Lighter `twap` parent order to the shared running-TWAP model. */
5
- export const mapRunningTwap = (order, market) => {
6
- const filledSize = new Big(order.filled_base_amount);
7
- return {
8
- twapId: String(order.order_index),
9
- market: toMarketDisplay(market),
10
- side: order.is_ask ? OrderSide.SELL : OrderSide.BUY,
11
- totalSize: order.initial_base_amount,
12
- filledSize: order.filled_base_amount,
13
- ...(filledSize.eq(0)
14
- ? {}
15
- : {
16
- avgFillPrice: new Big(order.filled_quote_amount)
17
- .div(filledSize)
18
- .toString(),
19
- }),
20
- startedAt: new Date(order.created_at * 1000).toISOString(),
21
- durationSeconds: (order.order_expiry - order.created_at * 1000) / 1000,
22
- status: TwapOrderStatus.RUNNING,
23
- };
24
- };
25
- //# sourceMappingURL=mapTwap.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapTwap.js","sourceRoot":"","sources":["../../../src/utils/mapTwap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAEL,SAAS,EAET,eAAe,GAChB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,GAAG,MAAM,QAAQ,CAAA;AAGxB,kFAAkF;AAClF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAc,EAAE,MAAc,EAAa,EAAE;IAC1E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACpD,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC;QACjC,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC;QAC/B,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG;QACnD,SAAS,EAAE,KAAK,CAAC,mBAAmB;QACpC,UAAU,EAAE,KAAK,CAAC,kBAAkB;QACpC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;YAClB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE,YAAY,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC;qBAC7C,GAAG,CAAC,UAAU,CAAC;qBACf,QAAQ,EAAE;aACd,CAAC;QACN,SAAS,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;QAC1D,eAAe,EAAE,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,IAAI;QACtE,MAAM,EAAE,eAAe,CAAC,OAAO;KAChC,CAAA;AACH,CAAC,CAAA"}
@@ -1,5 +0,0 @@
1
- import { type Market, type TwapOrder } from '@lifi/perps-types';
2
- import type { LtOrder } from '../types/order.js';
3
- /** Map an active Lighter `twap` parent order to the shared running-TWAP model. */
4
- export declare const mapRunningTwap: (order: LtOrder, market: Market) => TwapOrder;
5
- //# sourceMappingURL=mapTwap.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapTwap.d.ts","sourceRoot":"","sources":["../../../src/utils/mapTwap.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAA;AAE1B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAEhD,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,EAAE,QAAQ,MAAM,KAAG,SAmB/D,CAAA"}
@@ -1,31 +0,0 @@
1
- import { toMarketDisplay } from '@lifi/perps-sdk'
2
- import {
3
- type Market,
4
- OrderSide,
5
- type TwapOrder,
6
- TwapOrderStatus,
7
- } from '@lifi/perps-types'
8
- import Big from 'big.js'
9
- import type { LtOrder } from '../types/order.js'
10
-
11
- /** Map an active Lighter `twap` parent order to the shared running-TWAP model. */
12
- export const mapRunningTwap = (order: LtOrder, market: Market): TwapOrder => {
13
- const filledSize = new Big(order.filled_base_amount)
14
- return {
15
- twapId: String(order.order_index),
16
- market: toMarketDisplay(market),
17
- side: order.is_ask ? OrderSide.SELL : OrderSide.BUY,
18
- totalSize: order.initial_base_amount,
19
- filledSize: order.filled_base_amount,
20
- ...(filledSize.eq(0)
21
- ? {}
22
- : {
23
- avgFillPrice: new Big(order.filled_quote_amount)
24
- .div(filledSize)
25
- .toString(),
26
- }),
27
- startedAt: new Date(order.created_at * 1000).toISOString(),
28
- durationSeconds: (order.order_expiry - order.created_at * 1000) / 1000,
29
- status: TwapOrderStatus.RUNNING,
30
- }
31
- }