@lifi/perps-sdk-provider-lighter 36.0.0 → 37.0.1

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 (143) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +10 -10
  3. package/dist/cjs/LighterProvider.js.map +1 -1
  4. package/dist/cjs/accountSummary.d.ts.map +1 -1
  5. package/dist/cjs/accountSummary.js +13 -6
  6. package/dist/cjs/accountSummary.js.map +1 -1
  7. package/dist/cjs/types/activity.d.ts.map +1 -1
  8. package/dist/cjs/types/fill.d.ts +5 -0
  9. package/dist/cjs/types/fill.d.ts.map +1 -1
  10. package/dist/cjs/types/ws.d.ts.map +1 -1
  11. package/dist/cjs/utils/fundingRate.d.ts +2 -0
  12. package/dist/cjs/utils/fundingRate.d.ts.map +1 -0
  13. package/dist/cjs/utils/fundingRate.js +8 -0
  14. package/dist/cjs/utils/fundingRate.js.map +1 -0
  15. package/dist/cjs/utils/index.d.ts +1 -1
  16. package/dist/cjs/utils/index.d.ts.map +1 -1
  17. package/dist/cjs/utils/index.js +1 -3
  18. package/dist/cjs/utils/index.js.map +1 -1
  19. package/dist/cjs/utils/mapFill.js +3 -3
  20. package/dist/cjs/utils/mapFill.js.map +1 -1
  21. package/dist/cjs/utils/mapMarketContext.d.ts.map +1 -1
  22. package/dist/cjs/utils/mapMarketContext.js +13 -4
  23. package/dist/cjs/utils/mapMarketContext.js.map +1 -1
  24. package/dist/cjs/utils/mapOpenPositions.d.ts +1 -2
  25. package/dist/cjs/utils/mapOpenPositions.d.ts.map +1 -1
  26. package/dist/cjs/utils/mapOpenPositions.js +1 -13
  27. package/dist/cjs/utils/mapOpenPositions.js.map +1 -1
  28. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  29. package/dist/cjs/utils/mapOrder.js +31 -47
  30. package/dist/cjs/utils/mapOrder.js.map +1 -1
  31. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  32. package/dist/cjs/utils/mapPosition.js +34 -16
  33. package/dist/cjs/utils/mapPosition.js.map +1 -1
  34. package/dist/cjs/utils/transferMargin.js +1 -1
  35. package/dist/cjs/utils/transferMargin.js.map +1 -1
  36. package/dist/cjs/utils/wireEncoding.d.ts +0 -2
  37. package/dist/cjs/utils/wireEncoding.d.ts.map +1 -1
  38. package/dist/cjs/utils/wireEncoding.js +1 -15
  39. package/dist/cjs/utils/wireEncoding.js.map +1 -1
  40. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  41. package/dist/cjs/websocket/LighterWsProvider.js +46 -26
  42. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  43. package/dist/cjs/websocket/orderBookSide.d.ts +2 -1
  44. package/dist/cjs/websocket/orderBookSide.d.ts.map +1 -1
  45. package/dist/cjs/websocket/orderBookSide.js +5 -3
  46. package/dist/cjs/websocket/orderBookSide.js.map +1 -1
  47. package/dist/esm/LighterProvider.d.ts.map +1 -1
  48. package/dist/esm/LighterProvider.js +13 -13
  49. package/dist/esm/LighterProvider.js.map +1 -1
  50. package/dist/esm/accountSummary.d.ts.map +1 -1
  51. package/dist/esm/accountSummary.js +14 -7
  52. package/dist/esm/accountSummary.js.map +1 -1
  53. package/dist/esm/types/activity.d.ts +1 -0
  54. package/dist/esm/types/activity.d.ts.map +1 -1
  55. package/dist/esm/types/fill.d.ts +5 -0
  56. package/dist/esm/types/fill.d.ts.map +1 -1
  57. package/dist/esm/types/ws.d.ts +2 -0
  58. package/dist/esm/types/ws.d.ts.map +1 -1
  59. package/dist/esm/utils/fundingRate.d.ts +8 -0
  60. package/dist/esm/utils/fundingRate.d.ts.map +1 -0
  61. package/dist/esm/utils/fundingRate.js +10 -0
  62. package/dist/esm/utils/fundingRate.js.map +1 -0
  63. package/dist/esm/utils/index.d.ts +1 -1
  64. package/dist/esm/utils/index.d.ts.map +1 -1
  65. package/dist/esm/utils/index.js +1 -1
  66. package/dist/esm/utils/index.js.map +1 -1
  67. package/dist/esm/utils/mapFill.js +3 -3
  68. package/dist/esm/utils/mapFill.js.map +1 -1
  69. package/dist/esm/utils/mapMarketContext.d.ts +2 -1
  70. package/dist/esm/utils/mapMarketContext.d.ts.map +1 -1
  71. package/dist/esm/utils/mapMarketContext.js +15 -5
  72. package/dist/esm/utils/mapMarketContext.js.map +1 -1
  73. package/dist/esm/utils/mapOpenPositions.d.ts +1 -8
  74. package/dist/esm/utils/mapOpenPositions.d.ts.map +1 -1
  75. package/dist/esm/utils/mapOpenPositions.js +1 -18
  76. package/dist/esm/utils/mapOpenPositions.js.map +1 -1
  77. package/dist/esm/utils/mapOrder.d.ts +6 -3
  78. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  79. package/dist/esm/utils/mapOrder.js +39 -51
  80. package/dist/esm/utils/mapOrder.js.map +1 -1
  81. package/dist/esm/utils/mapPortfolioHistory.d.ts +2 -2
  82. package/dist/esm/utils/mapPortfolioHistory.js +2 -2
  83. package/dist/esm/utils/mapPosition.d.ts +4 -3
  84. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  85. package/dist/esm/utils/mapPosition.js +39 -20
  86. package/dist/esm/utils/mapPosition.js.map +1 -1
  87. package/dist/esm/utils/transferMargin.js +1 -1
  88. package/dist/esm/utils/transferMargin.js.map +1 -1
  89. package/dist/esm/utils/wireEncoding.d.ts +2 -28
  90. package/dist/esm/utils/wireEncoding.d.ts.map +1 -1
  91. package/dist/esm/utils/wireEncoding.js +2 -38
  92. package/dist/esm/utils/wireEncoding.js.map +1 -1
  93. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  94. package/dist/esm/websocket/LighterWsProvider.js +56 -27
  95. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  96. package/dist/esm/websocket/orderBookSide.d.ts +3 -2
  97. package/dist/esm/websocket/orderBookSide.d.ts.map +1 -1
  98. package/dist/esm/websocket/orderBookSide.js +5 -3
  99. package/dist/esm/websocket/orderBookSide.js.map +1 -1
  100. package/dist/types/LighterProvider.d.ts.map +1 -1
  101. package/dist/types/accountSummary.d.ts.map +1 -1
  102. package/dist/types/types/activity.d.ts +1 -0
  103. package/dist/types/types/activity.d.ts.map +1 -1
  104. package/dist/types/types/fill.d.ts +5 -0
  105. package/dist/types/types/fill.d.ts.map +1 -1
  106. package/dist/types/types/ws.d.ts +2 -0
  107. package/dist/types/types/ws.d.ts.map +1 -1
  108. package/dist/types/utils/fundingRate.d.ts +8 -0
  109. package/dist/types/utils/fundingRate.d.ts.map +1 -0
  110. package/dist/types/utils/index.d.ts +1 -1
  111. package/dist/types/utils/index.d.ts.map +1 -1
  112. package/dist/types/utils/mapMarketContext.d.ts +2 -1
  113. package/dist/types/utils/mapMarketContext.d.ts.map +1 -1
  114. package/dist/types/utils/mapOpenPositions.d.ts +1 -8
  115. package/dist/types/utils/mapOpenPositions.d.ts.map +1 -1
  116. package/dist/types/utils/mapOrder.d.ts +6 -3
  117. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  118. package/dist/types/utils/mapPortfolioHistory.d.ts +2 -2
  119. package/dist/types/utils/mapPosition.d.ts +4 -3
  120. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  121. package/dist/types/utils/wireEncoding.d.ts +2 -28
  122. package/dist/types/utils/wireEncoding.d.ts.map +1 -1
  123. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  124. package/dist/types/websocket/orderBookSide.d.ts +3 -2
  125. package/dist/types/websocket/orderBookSide.d.ts.map +1 -1
  126. package/package.json +3 -3
  127. package/src/LighterProvider.ts +22 -22
  128. package/src/accountSummary.ts +27 -6
  129. package/src/types/activity.ts +1 -0
  130. package/src/types/fill.ts +5 -0
  131. package/src/types/ws.ts +2 -0
  132. package/src/utils/fundingRate.ts +12 -0
  133. package/src/utils/index.ts +0 -2
  134. package/src/utils/mapFill.ts +3 -3
  135. package/src/utils/mapMarketContext.ts +18 -5
  136. package/src/utils/mapOpenPositions.ts +2 -30
  137. package/src/utils/mapOrder.ts +53 -71
  138. package/src/utils/mapPortfolioHistory.ts +2 -2
  139. package/src/utils/mapPosition.ts +50 -23
  140. package/src/utils/transferMargin.ts +1 -1
  141. package/src/utils/wireEncoding.ts +2 -43
  142. package/src/websocket/LighterWsProvider.ts +100 -60
  143. package/src/websocket/orderBookSide.ts +17 -4
@@ -1,10 +1,5 @@
1
- import { PerpsError, safeIsDecimalStringZero } from '@lifi/perps-sdk'
2
- import {
3
- PerpsErrorCode,
4
- type PerpsMarketDisplay,
5
- type Position,
6
- } from '@lifi/perps-types'
7
- import { LIGHTER_PROVIDER_KEY } from '../constants.js'
1
+ import { safeIsDecimalStringZero } from '@lifi/perps-sdk'
2
+ import type { PerpsMarketDisplay, Position } from '@lifi/perps-types'
8
3
  import type { LtAccountPosition } from '../types/index.js'
9
4
  import { mapPosition } from './mapPosition.js'
10
5
 
@@ -32,26 +27,3 @@ export const mapOpenPositions = (
32
27
  const position = mapPosition(p, resolveMarket(p.market_id))
33
28
  return position === undefined ? [] : [position]
34
29
  })
35
-
36
- /**
37
- * Map every open Lighter position for account totals, which must not leave a
38
- * position out.
39
- *
40
- * @throws {PerpsError} `SDKError` when `mapPosition` skips an open row.
41
- */
42
- export const requireOpenPositions = (
43
- positions: LtAccountPosition[],
44
- resolveMarket: (marketId: number) => PerpsMarketDisplay
45
- ): Position[] =>
46
- positions.filter(isOpenPosition).map((p) => {
47
- const position = mapPosition(p, resolveMarket(p.market_id))
48
- if (position !== undefined) {
49
- return position
50
- }
51
- const error = new PerpsError(
52
- PerpsErrorCode.SDKError,
53
- `${LIGHTER_PROVIDER_KEY} position for market ${p.market_id} is not valid, so the account totals are unknown.`
54
- )
55
- error.tool = LIGHTER_PROVIDER_KEY
56
- throw error
57
- })
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  isActiveOrderStatus,
3
- isDecimalStringGreaterThan,
4
3
  PerpsError,
5
4
  safeDivideDecimalString,
5
+ safeIsDecimalStringGreaterThan,
6
6
  safeTimestampToIsoString,
7
7
  triggerConditionFor,
8
- unknownToDecimalString,
9
8
  warnSkippedVenueRow,
9
+ wsLog,
10
10
  } from '@lifi/perps-sdk'
11
11
  import type { MarketDisplay, Order, OrderBase } from '@lifi/perps-types'
12
12
  import {
@@ -20,6 +20,9 @@ import { LIGHTER_PROVIDER_KEY } from '../constants.js'
20
20
  import type { LtOrder } from '../types/index.js'
21
21
  import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js'
22
22
 
23
+ // Lighter sends client index 0 as the string "0" for an order placed without one.
24
+ const NO_CLIENT_ORDER_ID = '0'
25
+
23
26
  const mapOrderType = (type: string): Order['type'] => {
24
27
  switch (type.replace(/-/g, '_')) {
25
28
  case 'market':
@@ -62,18 +65,8 @@ const mapTimeInForce = (tif: string): TimeInForce => {
62
65
  }
63
66
  }
64
67
 
65
- const venueDecimal = (value: unknown, field: string): string | undefined => {
66
- try {
67
- return unknownToDecimalString(value, field, LIGHTER_PROVIDER_KEY)
68
- } catch {
69
- return undefined
70
- }
71
- }
72
-
73
- const hasFill = (order: LtOrder): boolean => {
74
- const filled = venueDecimal(order.filled_base_amount, 'filled_base_amount')
75
- return filled !== undefined && isDecimalStringGreaterThan(filled, '0')
76
- }
68
+ const hasFill = (order: LtOrder): boolean =>
69
+ safeIsDecimalStringGreaterThan(order.filled_base_amount, '0') === true
77
70
 
78
71
  const mapOrderStatus = (order: LtOrder): OrderStatus => {
79
72
  switch (order.status) {
@@ -113,84 +106,56 @@ const mapOrderStatus = (order: LtOrder): OrderStatus => {
113
106
  }
114
107
  }
115
108
 
116
- const skipOrder = (field: string, value: unknown): undefined => {
117
- warnSkippedVenueRow(LIGHTER_PROVIDER_KEY, 'order', field, value)
109
+ const skipOrder = (
110
+ marketId: string,
111
+ field: string,
112
+ value: unknown
113
+ ): undefined => {
114
+ warnSkippedVenueRow(LIGHTER_PROVIDER_KEY, 'order', field, value, { marketId })
118
115
  return undefined
119
116
  }
120
117
 
121
- const orderSizeOrSkip = (
122
- field: 'initial_base_amount' | 'remaining_base_amount' | 'filled_base_amount',
123
- value: string
124
- ): string | undefined =>
125
- venueDecimal(value, field) === undefined ? skipOrder(field, value) : value
126
-
127
118
  /**
128
119
  * Map a Lighter order with its venue identity, lifecycle, and execution fields.
129
- * A row with an invalid size or time gives `undefined`. An invalid limit price or
130
- * filled quote amount omits that field.
120
+ * Venue sizes and prices pass verbatim. A row with an invalid time gives
121
+ * `undefined`. An average price that cannot be derived is omitted.
131
122
  */
132
123
  export const mapOrder = (
133
124
  order: LtOrder,
134
125
  market: MarketDisplay
135
126
  ): Order | undefined => {
136
127
  const type = mapOrderType(order.type)
137
- const originalSize = orderSizeOrSkip(
138
- 'initial_base_amount',
139
- order.initial_base_amount
140
- )
141
- if (originalSize === undefined) {
142
- return undefined
143
- }
144
- const remainingSize = orderSizeOrSkip(
145
- 'remaining_base_amount',
146
- order.remaining_base_amount
147
- )
148
- if (remainingSize === undefined) {
149
- return undefined
150
- }
151
- const filledSize = orderSizeOrSkip(
152
- 'filled_base_amount',
153
- order.filled_base_amount
154
- )
155
- if (filledSize === undefined) {
156
- return undefined
157
- }
158
128
  const createdAt = rowTimestampToIsoStringOrUndefined(order.created_at * 1000)
159
129
  if (createdAt === undefined) {
160
- return skipOrder('created_at', order.created_at)
130
+ return skipOrder(market.id, 'created_at', order.created_at)
161
131
  }
162
132
  const updatedAt = rowTimestampToIsoStringOrUndefined(order.updated_at * 1000)
163
133
  if (updatedAt === undefined) {
164
- return skipOrder('updated_at', order.updated_at)
134
+ return skipOrder(market.id, 'updated_at', order.updated_at)
165
135
  }
166
- const filledQuote = venueDecimal(
167
- order.filled_quote_amount,
168
- 'filled_quote_amount'
169
- )
170
- const filledAmount = venueDecimal(filledSize, 'filled_base_amount')
171
- const averagePrice =
172
- filledQuote !== undefined &&
173
- filledAmount !== undefined &&
174
- isDecimalStringGreaterThan(filledAmount, '0')
175
- ? safeDivideDecimalString(filledQuote, filledAmount)
176
- : undefined
136
+ const averagePrice = hasFill(order)
137
+ ? safeDivideDecimalString(
138
+ order.filled_quote_amount,
139
+ order.filled_base_amount
140
+ )
141
+ : undefined
177
142
  const expiresAt =
178
143
  order.order_expiry > 0
179
144
  ? safeTimestampToIsoString(order.order_expiry)
180
145
  : undefined
181
146
  const status = mapOrderStatus(order)
182
147
  const base: OrderBase = {
183
- orderId: String(order.order_index),
184
- ...(order.client_order_index === 0
148
+ orderId: order.order_id,
149
+ ...(order.client_order_id === NO_CLIENT_ORDER_ID
185
150
  ? {}
186
- : { clientOrderId: String(order.client_order_index) }),
151
+ : { clientOrderId: order.client_order_id }),
187
152
  market,
188
153
  side: order.is_ask ? OrderSide.SELL : OrderSide.BUY,
189
154
  status,
190
155
  ...(status === OrderStatus.CANCELLED ? { statusReason: order.status } : {}),
191
- originalSize,
192
- remainingSize,
193
- filledSize,
156
+ originalSize: order.initial_base_amount,
157
+ remainingSize: order.remaining_base_amount,
158
+ filledSize: order.filled_base_amount,
194
159
  ...(averagePrice === undefined ? {} : { averagePrice }),
195
160
  reduceOnly: order.reduce_only,
196
161
  ...(order.parent_order_id ? { parentOrderId: order.parent_order_id } : {}),
@@ -214,9 +179,8 @@ export const mapOrder = (
214
179
  type,
215
180
  triggerPrice: order.trigger_price,
216
181
  triggerCondition: triggerConditionFor(type, base.side),
217
- ...((type === OrderType.STOP_LIMIT ||
218
- type === OrderType.TAKE_PROFIT_LIMIT) &&
219
- venueDecimal(order.price, 'price') !== undefined
182
+ ...(type === OrderType.STOP_LIMIT ||
183
+ type === OrderType.TAKE_PROFIT_LIMIT
220
184
  ? { limitPrice: order.price }
221
185
  : {}),
222
186
  }
@@ -233,7 +197,10 @@ export const mapOrder = (
233
197
  }
234
198
  }
235
199
 
236
- /** Include terminal rows and their ids so consumers can update history and active caches. */
200
+ /**
201
+ * Include terminal rows and their ids so consumers can update history and
202
+ * active caches. A row the mapper rejects is dropped and warns once.
203
+ */
237
204
  export const mapOrderUpdates = (
238
205
  rawOrders: LtOrder[],
239
206
  resolveMarket: (marketIndex: number) => MarketDisplay | undefined
@@ -242,9 +209,24 @@ export const mapOrderUpdates = (
242
209
  const terminated: string[] = []
243
210
  for (const raw of rawOrders) {
244
211
  const market = resolveMarket(raw.market_index)
245
- const order = market === undefined ? undefined : mapOrder(raw, market)
246
- if (!isActiveOrderStatus(order?.status ?? mapOrderStatus(raw))) {
247
- terminated.push(String(raw.order_index))
212
+ let order: Order | undefined
213
+ let status: OrderStatus
214
+ try {
215
+ order = market === undefined ? undefined : mapOrder(raw, market)
216
+ status = order?.status ?? mapOrderStatus(raw)
217
+ } catch (error) {
218
+ if (!(error instanceof PerpsError)) {
219
+ throw error
220
+ }
221
+ wsLog.droppedRow(
222
+ LIGHTER_PROVIDER_KEY,
223
+ 'order',
224
+ `${raw.order_id}: ${error.message}`
225
+ )
226
+ continue
227
+ }
228
+ if (!isActiveOrderStatus(status)) {
229
+ terminated.push(raw.order_id)
248
230
  }
249
231
  if (order !== undefined) {
250
232
  orders.push(order)
@@ -69,8 +69,8 @@ function getCumulativeNetFlow(snapshot: LtPnLEntry): string {
69
69
  * Historical account values are anchored to the current account value. A
70
70
  * snapshot with a non-finite value is skipped.
71
71
  *
72
- * @throws {PerpsError} `ValidationError` when `currentValue` is not a decimal
73
- * string.
72
+ * @throws {PerpsError} `ValidationError` when `currentValue` does not match the
73
+ * decimal pattern.
74
74
  */
75
75
  export const mapPortfolioHistory = (
76
76
  range: PortfolioHistoryRange,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  absDecimalString,
3
+ createWarnOnce,
3
4
  divideDecimalString,
4
5
  isDecimalString,
5
6
  isDecimalStringZero,
@@ -53,16 +54,32 @@ export const leverageFromScaledImf = (imf: number): string | undefined => {
53
54
  return percent === undefined ? undefined : leverageFromImf(percent)
54
55
  }
55
56
 
56
- const skipPosition = (field: string, value: unknown): undefined => {
57
- warnSkippedVenueRow(LIGHTER_PROVIDER_KEY, 'position', field, value)
57
+ const skipPosition = (
58
+ marketId: string,
59
+ field: string,
60
+ value: unknown
61
+ ): undefined => {
62
+ warnSkippedVenueRow(LIGHTER_PROVIDER_KEY, 'position', field, value, {
63
+ marketId,
64
+ })
58
65
  return undefined
59
66
  }
60
67
 
68
+ const warnUnreadableFieldOnce = createWarnOnce()
69
+
70
+ const warnUnreadableField = (field: string, value: unknown): void => {
71
+ warnUnreadableFieldOnce(
72
+ `${LIGHTER_PROVIDER_KEY}|position.${field}`,
73
+ `[${LIGHTER_PROVIDER_KEY}] position \`${field}\` is not readable, so the derived fields are absent: '${String(value).slice(0, 64)}'`
74
+ )
75
+ }
76
+
61
77
  /**
62
78
  * Map a raw Lighter account position to the generic Position type. A row with
63
- * an invalid size or value, or a non-positive initial margin fraction, gives
64
- * `undefined`: the mapper derives the size, mark price, margin and leverage
65
- * from them. Prices, PnL, funding and isolated margin pass through raw.
79
+ * an invalid size gives `undefined`. An invalid position value or a
80
+ * non-positive initial margin fraction keeps the row, leaves `leverage` and
81
+ * `initialMarginRequirement` absent, sets a cross `marginUsed` to `'0'` and
82
+ * warns once. Prices, PnL, funding and isolated margin pass through raw.
66
83
  * @param market - Backend-resolved market identity for `pos.market_id`.
67
84
  * @public
68
85
  */
@@ -71,39 +88,49 @@ export const mapPosition = (
71
88
  market: PerpsMarketDisplay
72
89
  ): Position | undefined => {
73
90
  if (!isDecimalString(pos.position)) {
74
- return skipPosition('position', pos.position)
91
+ return skipPosition(market.id, 'position', pos.position)
75
92
  }
76
- if (!isDecimalString(pos.position_value)) {
77
- return skipPosition('position_value', pos.position_value)
93
+ const isIsolated = pos.margin_mode === LT_MARGIN_MODE_ISOLATED
94
+ const size = absDecimalString(pos.position)
95
+ const positionValue = isDecimalString(pos.position_value)
96
+ ? absDecimalString(pos.position_value)
97
+ : undefined
98
+ if (positionValue === undefined) {
99
+ warnUnreadableField('position_value', pos.position_value)
78
100
  }
79
101
  const imf = pos.initial_margin_fraction
80
102
  const leverage = isDecimalString(imf) ? leverageFromImf(imf) : undefined
81
103
  if (leverage === undefined) {
82
- return skipPosition('initial_margin_fraction', imf)
104
+ warnUnreadableField('initial_margin_fraction', imf)
83
105
  }
84
- const isIsolated = pos.margin_mode === LT_MARGIN_MODE_ISOLATED
85
- const size = absDecimalString(pos.position)
86
- const positionValue = absDecimalString(pos.position_value)
87
- const initialMarginRequirement = divideDecimalString(
88
- multiplyDecimalString(positionValue, imf),
89
- '100'
90
- )
106
+ const initialMarginRequirement =
107
+ positionValue === undefined || leverage === undefined
108
+ ? undefined
109
+ : divideDecimalString(multiplyDecimalString(positionValue, imf), '100')
91
110
 
92
111
  return {
93
112
  market,
94
113
  side: pos.sign >= 0 ? PositionSide.LONG : PositionSide.SHORT,
95
114
  size,
96
115
  entryPrice: pos.avg_entry_price,
97
- markPrice:
98
- isDecimalStringZero(positionValue) || isDecimalStringZero(size)
99
- ? '0'
100
- : divideDecimalString(positionValue, size),
116
+ ...(positionValue === undefined
117
+ ? {}
118
+ : {
119
+ markPrice:
120
+ isDecimalStringZero(positionValue) || isDecimalStringZero(size)
121
+ ? '0'
122
+ : divideDecimalString(positionValue, size),
123
+ }),
101
124
  liquidationPrice: pos.liquidation_price,
102
125
  unrealizedPnl: pos.unrealized_pnl,
103
126
  accruedFunding: pos.total_funding_paid_out ?? '0',
104
- leverage,
105
- marginUsed: isIsolated ? pos.allocated_margin : initialMarginRequirement,
106
- initialMarginRequirement,
127
+ ...(leverage === undefined ? {} : { leverage }),
128
+ marginUsed: isIsolated
129
+ ? pos.allocated_margin
130
+ : (initialMarginRequirement ?? '0'),
131
+ ...(initialMarginRequirement === undefined
132
+ ? {}
133
+ : { initialMarginRequirement }),
107
134
  marginMode: isIsolated ? MarginMode.ISOLATED : MarginMode.CROSS,
108
135
  }
109
136
  }
@@ -23,7 +23,7 @@ function positionAmount(
23
23
  if (!isDecimalString(value)) {
24
24
  throw new PerpsError(
25
25
  PerpsErrorCode.ValidationError,
26
- `Invalid \`Position.${field}\`: '${value}' is not a decimal string.`
26
+ `Invalid \`Position.${field}\`: '${value}' does not match the decimal pattern.`
27
27
  )
28
28
  }
29
29
  return value
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Lighter wire encoding for the WASM signer: basis-point margin fractions and
3
- * order-type/time-in-force/expiry resolution mirroring lighter-go's rules.
2
+ * Lighter wire encoding for the WASM signer: order-type/time-in-force/expiry
3
+ * resolution mirroring lighter-go's rules.
4
4
  */
5
5
 
6
6
  import { PerpsError } from '@lifi/perps-sdk'
@@ -19,47 +19,6 @@ import {
19
19
  LT_TIME_IN_FORCE_POST_ONLY,
20
20
  } from '../types/action.js'
21
21
 
22
- /**
23
- * Convert Lighter's initial-margin fraction to the largest whole-number
24
- * leverage exposed by the provider.
25
- *
26
- * Lighter expresses margin fractions in basis points (`10_000` = 100%), so
27
- * the conversion is `floor(10_000 / fraction)`. Non-finite or non-positive
28
- * input uses the safe `1x` fallback.
29
- *
30
- * @param fraction - Lighter's basis-point initial-margin fraction.
31
- * @returns The floored maximum leverage, or `1` for invalid input.
32
- * @public
33
- */
34
- export const marginFractionToMaxLeverage = (fraction: number): number => {
35
- if (!Number.isFinite(fraction) || fraction <= 0) {
36
- return 1
37
- }
38
- return Math.floor(10_000 / fraction)
39
- }
40
-
41
- /**
42
- * Convert requested leverage to Lighter's basis-point margin fraction.
43
- *
44
- * The signer expects `round(10_000 / leverage)`, where the result is the
45
- * margin requirement in basis points.
46
- *
47
- * @param leverage - Requested leverage multiplier.
48
- * @returns The rounded basis-point margin fraction.
49
- * @throws {PerpsError} With `ValidationError` when leverage is less than
50
- * or equal to zero.
51
- * @public
52
- */
53
- export const leverageToFraction = (leverage: number): number => {
54
- if (leverage <= 0) {
55
- throw new PerpsError(
56
- PerpsErrorCode.ValidationError,
57
- `Invalid leverage: ${leverage}`
58
- )
59
- }
60
- return Math.round(10_000 / leverage)
61
- }
62
-
63
22
  /**
64
23
  * Map a LI.FI `OrderType` value to Lighter's integer wire type.
65
24
  * Supported mappings are LIMIT → 0, MARKET → 1, STOP_MARKET → 2,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  addDecimalString,
3
3
  cachePromise,
4
+ createSafeFunction,
4
5
  getMarketRegistry,
5
6
  isDecimalStringGreaterThan,
6
7
  type MarketRegistry,
@@ -12,6 +13,7 @@ import {
12
13
  ReconnectingWebSocket,
13
14
  resolveRetryPolicy,
14
15
  resolveSubscribeQuote,
16
+ safeUnknownToDecimalString,
15
17
  subtractDecimalString,
16
18
  toPerpsMarketDisplay,
17
19
  unknownToDecimalString,
@@ -23,6 +25,7 @@ import {
23
25
  import {
24
26
  type AccountSummary,
25
27
  type Fill,
28
+ type Market,
26
29
  type MarketContext,
27
30
  PerpsErrorCode,
28
31
  type Position,
@@ -172,6 +175,57 @@ export interface LighterWsProviderOptions {
172
175
  resolveAuthToken?: LighterAuthTokenResolver
173
176
  }
174
177
 
178
+ /**
179
+ * Perps equity plus the USD value of every positive held balance. A non-
180
+ * settlement asset counts its spot and margin routes; the settlement asset
181
+ * counts its spot route only, because the equity already holds its margin.
182
+ *
183
+ * @throws {PerpsError} `SDKError` when a held asset's spot mark does not
184
+ * match the decimal pattern.
185
+ */
186
+ const heldPortfolioValue = (
187
+ perpsEquity: string,
188
+ balances: ReadonlyMap<number, { spot: string; margin: string }>,
189
+ settlementAssetIndex: number,
190
+ markets: readonly Market[],
191
+ contexts: readonly MarketContext[]
192
+ ): string => {
193
+ const held = [...balances]
194
+ .map(
195
+ ([assetId, { spot, margin }]) =>
196
+ [
197
+ assetId,
198
+ assetId === settlementAssetIndex
199
+ ? spot
200
+ : addDecimalString(spot, margin),
201
+ ] as const
202
+ )
203
+ .filter(([, balance]) => isDecimalStringGreaterThan(balance, '0'))
204
+ const spotPrices = spotPriceByAssetId(
205
+ markets,
206
+ LIGHTER_SPOT_CATEGORY_ID,
207
+ contexts,
208
+ new Set(
209
+ held
210
+ .filter(([assetId]) => assetId !== settlementAssetIndex)
211
+ .map(([assetId]) => String(assetId))
212
+ )
213
+ )
214
+ return lighterPortfolioValue(
215
+ perpsEquity,
216
+ held.map(
217
+ ([assetId, balance]) =>
218
+ spotValuation(assetId, balance, settlementAssetIndex, spotPrices)
219
+ .valueUsd
220
+ )
221
+ )
222
+ }
223
+
224
+ const safeHeldPortfolioValue = createSafeFunction(
225
+ 'heldPortfolioValue',
226
+ heldPortfolioValue
227
+ )
228
+
175
229
  /**
176
230
  * Lighter WebSocket provider (extends {@link WsProviderBase}): subscribes to
177
231
  * Lighter's WS channels (orderbook, marketsContext, orders, positions), attaching auth
@@ -723,47 +777,53 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
723
777
  return
724
778
  }
725
779
 
726
- const portfolio = unknownToDecimalString(
727
- stats.portfolio_value,
728
- 'stats.portfolio_value',
729
- this.providerKey
730
- )
731
- const collateral = unknownToDecimalString(
732
- stats.collateral,
733
- 'stats.collateral',
734
- this.providerKey
735
- )
780
+ const read = (value: unknown, field: string): string | undefined =>
781
+ safeUnknownToDecimalString(value, field, this.providerKey)
782
+ const portfolio = read(stats.portfolio_value, 'stats.portfolio_value')
783
+ const collateral = read(stats.collateral, 'stats.collateral')
736
784
 
737
- let available: string
738
- let marginUsed: string
785
+ let available: string | undefined
786
+ let marginUsed: string | undefined
739
787
  if (stats.cross_stats === undefined) {
740
788
  // Compatibility with legacy gateways that omitted the entire
741
789
  // cross_stats object.
742
- available = unknownToDecimalString(
743
- stats.available_balance,
744
- 'stats.available_balance',
745
- this.providerKey
746
- )
747
- marginUsed = subtractDecimalString(portfolio, available)
790
+ available = read(stats.available_balance, 'stats.available_balance')
791
+ marginUsed =
792
+ portfolio === undefined || available === undefined
793
+ ? undefined
794
+ : subtractDecimalString(portfolio, available)
748
795
  } else {
749
- const crossCollateral = unknownToDecimalString(
796
+ const crossCollateral = read(
750
797
  stats.cross_stats.collateral,
751
- 'stats.cross_stats.collateral',
752
- this.providerKey
798
+ 'stats.cross_stats.collateral'
753
799
  )
754
- const crossPortfolio = unknownToDecimalString(
800
+ const crossPortfolio = read(
755
801
  stats.cross_stats.portfolio_value,
756
- 'stats.cross_stats.portfolio_value',
757
- this.providerKey
802
+ 'stats.cross_stats.portfolio_value'
758
803
  )
759
- available = unknownToDecimalString(
804
+ available = read(
760
805
  stats.cross_stats.available_balance,
761
- 'stats.cross_stats.available_balance',
762
- this.providerKey
806
+ 'stats.cross_stats.available_balance'
763
807
  )
764
- const isolatedMargin = subtractDecimalString(collateral, crossCollateral)
765
- const crossMargin = subtractDecimalString(crossPortfolio, available)
766
- marginUsed = addDecimalString(isolatedMargin, crossMargin)
808
+ marginUsed =
809
+ collateral === undefined ||
810
+ crossCollateral === undefined ||
811
+ crossPortfolio === undefined ||
812
+ available === undefined
813
+ ? undefined
814
+ : addDecimalString(
815
+ subtractDecimalString(collateral, crossCollateral),
816
+ subtractDecimalString(crossPortfolio, available)
817
+ )
818
+ }
819
+ // Every AccountSummary field is required, so the last summary stays.
820
+ if (
821
+ portfolio === undefined ||
822
+ collateral === undefined ||
823
+ available === undefined ||
824
+ marginUsed === undefined
825
+ ) {
826
+ return
767
827
  }
768
828
 
769
829
  const inputs = this.accountSummaryInputs.get(address)
@@ -827,36 +887,16 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
827
887
  if (inputs?.perps === undefined || inputs.balances === undefined) {
828
888
  return
829
889
  }
830
- const settlementAssetIndex = this.settlementAssetIndex()
831
- const held = [...inputs.balances]
832
- .map(
833
- ([assetId, { spot, margin }]) =>
834
- [
835
- assetId,
836
- assetId === settlementAssetIndex
837
- ? spot
838
- : addDecimalString(spot, margin),
839
- ] as const
840
- )
841
- .filter(([, balance]) => isDecimalStringGreaterThan(balance, '0'))
842
- const spotPrices = spotPriceByAssetId(
843
- this.registry?.markets ?? [],
844
- LIGHTER_SPOT_CATEGORY_ID,
845
- Object.values(this.marketsContext),
846
- new Set(
847
- held
848
- .filter(([assetId]) => assetId !== settlementAssetIndex)
849
- .map(([assetId]) => String(assetId))
850
- )
851
- )
852
- const portfolioValue = lighterPortfolioValue(
890
+ const portfolioValue = safeHeldPortfolioValue(
853
891
  inputs.perps.equity,
854
- held.map(
855
- ([assetId, balance]) =>
856
- spotValuation(assetId, balance, settlementAssetIndex, spotPrices)
857
- .valueUsd
858
- )
892
+ inputs.balances,
893
+ this.settlementAssetIndex(),
894
+ this.registry?.markets ?? [],
895
+ Object.values(this.marketsContext)
859
896
  )
897
+ if (portfolioValue === undefined) {
898
+ return
899
+ }
860
900
  if (onlyOnChange && portfolioValue === inputs.lastPortfolioValue) {
861
901
  return
862
902
  }
@@ -1017,8 +1057,8 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
1017
1057
  let state = this.orderbooks.get(marketId)
1018
1058
  if (!state || isSnapshot) {
1019
1059
  state = {
1020
- bids: new OrderBookSide(bidOrder, this.providerKey),
1021
- asks: new OrderBookSide(askOrder, this.providerKey),
1060
+ bids: new OrderBookSide(bidOrder, this.providerKey, assetId),
1061
+ asks: new OrderBookSide(askOrder, this.providerKey, assetId),
1022
1062
  assetId,
1023
1063
  }
1024
1064
  this.orderbooks.set(marketId, state)