@lifi/perps-sdk-provider-lighter 30.0.0 → 31.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 (138) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +62 -54
  3. package/dist/cjs/LighterProvider.js.map +1 -1
  4. package/dist/cjs/accountSummary.d.ts +4 -1
  5. package/dist/cjs/accountSummary.d.ts.map +1 -1
  6. package/dist/cjs/accountSummary.js +8 -7
  7. package/dist/cjs/accountSummary.js.map +1 -1
  8. package/dist/cjs/availableToTrade.d.ts +2 -0
  9. package/dist/cjs/availableToTrade.d.ts.map +1 -1
  10. package/dist/cjs/availableToTrade.js +6 -7
  11. package/dist/cjs/availableToTrade.js.map +1 -1
  12. package/dist/cjs/types/pnl.d.ts.map +1 -1
  13. package/dist/cjs/types/ws.d.ts +5 -0
  14. package/dist/cjs/types/ws.d.ts.map +1 -1
  15. package/dist/cjs/utils/apiClient.d.ts +3 -2
  16. package/dist/cjs/utils/apiClient.d.ts.map +1 -1
  17. package/dist/cjs/utils/apiClient.js +5 -3
  18. package/dist/cjs/utils/apiClient.js.map +1 -1
  19. package/dist/cjs/utils/fetchDetailedAccount.d.ts +1 -1
  20. package/dist/cjs/utils/fetchDetailedAccount.d.ts.map +1 -1
  21. package/dist/cjs/utils/fetchDetailedAccount.js +2 -1
  22. package/dist/cjs/utils/fetchDetailedAccount.js.map +1 -1
  23. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  24. package/dist/cjs/utils/mapFill.js +1 -3
  25. package/dist/cjs/utils/mapFill.js.map +1 -1
  26. package/dist/cjs/utils/mapPortfolioHistory.d.ts +1 -1
  27. package/dist/cjs/utils/mapPortfolioHistory.d.ts.map +1 -1
  28. package/dist/cjs/utils/mapPortfolioHistory.js +36 -21
  29. package/dist/cjs/utils/mapPortfolioHistory.js.map +1 -1
  30. package/dist/cjs/utils/mapPosition.d.ts +1 -0
  31. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  32. package/dist/cjs/utils/mapPosition.js +13 -1
  33. package/dist/cjs/utils/mapPosition.js.map +1 -1
  34. package/dist/cjs/utils/ohlcvInterval.d.ts.map +1 -1
  35. package/dist/cjs/utils/ohlcvInterval.js.map +1 -1
  36. package/dist/cjs/utils/spotPrice.d.ts +5 -1
  37. package/dist/cjs/utils/spotPrice.d.ts.map +1 -1
  38. package/dist/cjs/utils/spotPrice.js +14 -1
  39. package/dist/cjs/utils/spotPrice.js.map +1 -1
  40. package/dist/cjs/utils/withdrawableBalances.d.ts +3 -3
  41. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
  42. package/dist/cjs/utils/withdrawableBalances.js +11 -3
  43. package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
  44. package/dist/cjs/websocket/LighterWsProvider.d.ts +6 -0
  45. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  46. package/dist/cjs/websocket/LighterWsProvider.js +120 -9
  47. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  48. package/dist/esm/LighterProvider.d.ts.map +1 -1
  49. package/dist/esm/LighterProvider.js +71 -61
  50. package/dist/esm/LighterProvider.js.map +1 -1
  51. package/dist/esm/accountSummary.d.ts +17 -3
  52. package/dist/esm/accountSummary.d.ts.map +1 -1
  53. package/dist/esm/accountSummary.js +18 -9
  54. package/dist/esm/accountSummary.js.map +1 -1
  55. package/dist/esm/availableToTrade.d.ts +2 -0
  56. package/dist/esm/availableToTrade.d.ts.map +1 -1
  57. package/dist/esm/availableToTrade.js +4 -6
  58. package/dist/esm/availableToTrade.js.map +1 -1
  59. package/dist/esm/types/pnl.d.ts +2 -3
  60. package/dist/esm/types/pnl.d.ts.map +1 -1
  61. package/dist/esm/types/ws.d.ts +13 -0
  62. package/dist/esm/types/ws.d.ts.map +1 -1
  63. package/dist/esm/types/ws.js +0 -11
  64. package/dist/esm/types/ws.js.map +1 -1
  65. package/dist/esm/utils/apiClient.d.ts +9 -2
  66. package/dist/esm/utils/apiClient.d.ts.map +1 -1
  67. package/dist/esm/utils/apiClient.js +9 -2
  68. package/dist/esm/utils/apiClient.js.map +1 -1
  69. package/dist/esm/utils/fetchDetailedAccount.d.ts +1 -1
  70. package/dist/esm/utils/fetchDetailedAccount.d.ts.map +1 -1
  71. package/dist/esm/utils/fetchDetailedAccount.js +2 -1
  72. package/dist/esm/utils/fetchDetailedAccount.js.map +1 -1
  73. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  74. package/dist/esm/utils/mapFill.js +6 -10
  75. package/dist/esm/utils/mapFill.js.map +1 -1
  76. package/dist/esm/utils/mapPortfolioHistory.d.ts +3 -6
  77. package/dist/esm/utils/mapPortfolioHistory.d.ts.map +1 -1
  78. package/dist/esm/utils/mapPortfolioHistory.js +38 -26
  79. package/dist/esm/utils/mapPortfolioHistory.js.map +1 -1
  80. package/dist/esm/utils/mapPosition.d.ts +7 -0
  81. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  82. package/dist/esm/utils/mapPosition.js +17 -0
  83. package/dist/esm/utils/mapPosition.js.map +1 -1
  84. package/dist/esm/utils/ohlcvInterval.d.ts.map +1 -1
  85. package/dist/esm/utils/ohlcvInterval.js +5 -3
  86. package/dist/esm/utils/ohlcvInterval.js.map +1 -1
  87. package/dist/esm/utils/spotPrice.d.ts +10 -1
  88. package/dist/esm/utils/spotPrice.d.ts.map +1 -1
  89. package/dist/esm/utils/spotPrice.js +14 -0
  90. package/dist/esm/utils/spotPrice.js.map +1 -1
  91. package/dist/esm/utils/withdrawableBalances.d.ts +12 -5
  92. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
  93. package/dist/esm/utils/withdrawableBalances.js +17 -5
  94. package/dist/esm/utils/withdrawableBalances.js.map +1 -1
  95. package/dist/esm/websocket/LighterWsProvider.d.ts +24 -0
  96. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  97. package/dist/esm/websocket/LighterWsProvider.js +153 -15
  98. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  99. package/dist/types/LighterProvider.d.ts.map +1 -1
  100. package/dist/types/accountSummary.d.ts +17 -3
  101. package/dist/types/accountSummary.d.ts.map +1 -1
  102. package/dist/types/availableToTrade.d.ts +2 -0
  103. package/dist/types/availableToTrade.d.ts.map +1 -1
  104. package/dist/types/types/pnl.d.ts +2 -3
  105. package/dist/types/types/pnl.d.ts.map +1 -1
  106. package/dist/types/types/ws.d.ts +13 -0
  107. package/dist/types/types/ws.d.ts.map +1 -1
  108. package/dist/types/utils/apiClient.d.ts +9 -2
  109. package/dist/types/utils/apiClient.d.ts.map +1 -1
  110. package/dist/types/utils/fetchDetailedAccount.d.ts +1 -1
  111. package/dist/types/utils/fetchDetailedAccount.d.ts.map +1 -1
  112. package/dist/types/utils/mapFill.d.ts.map +1 -1
  113. package/dist/types/utils/mapPortfolioHistory.d.ts +3 -6
  114. package/dist/types/utils/mapPortfolioHistory.d.ts.map +1 -1
  115. package/dist/types/utils/mapPosition.d.ts +7 -0
  116. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  117. package/dist/types/utils/ohlcvInterval.d.ts.map +1 -1
  118. package/dist/types/utils/spotPrice.d.ts +10 -1
  119. package/dist/types/utils/spotPrice.d.ts.map +1 -1
  120. package/dist/types/utils/withdrawableBalances.d.ts +12 -5
  121. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
  122. package/dist/types/websocket/LighterWsProvider.d.ts +24 -0
  123. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  124. package/package.json +3 -3
  125. package/src/LighterProvider.ts +119 -74
  126. package/src/accountSummary.ts +37 -14
  127. package/src/availableToTrade.ts +6 -10
  128. package/src/types/pnl.ts +2 -3
  129. package/src/types/ws.ts +23 -0
  130. package/src/utils/apiClient.ts +16 -7
  131. package/src/utils/fetchDetailedAccount.ts +2 -2
  132. package/src/utils/mapFill.ts +6 -13
  133. package/src/utils/mapPortfolioHistory.ts +42 -27
  134. package/src/utils/mapPosition.ts +17 -0
  135. package/src/utils/ohlcvInterval.ts +5 -3
  136. package/src/utils/spotPrice.ts +21 -1
  137. package/src/utils/withdrawableBalances.ts +26 -7
  138. package/src/websocket/LighterWsProvider.ts +222 -14
@@ -15,6 +15,15 @@ import { lighterErrorCodeFromBody } from './lighterErrorCode.js'
15
15
  /** @internal */
16
16
  export type ApiParams = Record<string, string | number | boolean>
17
17
 
18
+ /**
19
+ * Short excerpt of a parsed Lighter body for an error message. `undefined`
20
+ * stands for a body that did not parse as JSON.
21
+ *
22
+ * @internal
23
+ */
24
+ export const lighterBodyExcerpt = (data: unknown): string =>
25
+ data === undefined ? 'non-JSON body' : JSON.stringify(data).slice(0, 200)
26
+
18
27
  /**
19
28
  * Lighter signals errors on two channels: a non-2xx HTTP status, or an HTTP 200
20
29
  * carrying an error `code` in the JSON body. Returns the body error `code` when
@@ -269,7 +278,7 @@ export class LighterApiClient {
269
278
  async getWithStatus<T>(
270
279
  path: string,
271
280
  params?: ApiParams
272
- ): Promise<{ status: number; data: T }> {
281
+ ): Promise<{ status: number; data: T | undefined }> {
273
282
  return this.sendGet<T>(path, params)
274
283
  }
275
284
 
@@ -277,7 +286,7 @@ export class LighterApiClient {
277
286
  path: string,
278
287
  params?: ApiParams,
279
288
  headers?: Record<string, string>
280
- ): Promise<{ status: number; data: T }> {
289
+ ): Promise<{ status: number; data: T | undefined }> {
281
290
  this.assertRequestAllowed()
282
291
  const url = this.buildUrl(path, params)
283
292
  const response = await fetchWithRetry(
@@ -290,7 +299,7 @@ export class LighterApiClient {
290
299
  }
291
300
  )
292
301
  this.assertNotRateLimited(response)
293
- const data = (await response.json().catch(() => undefined)) as T
302
+ const data = (await response.json().catch(() => undefined)) as T | undefined
294
303
  return { status: response.status, data }
295
304
  }
296
305
 
@@ -305,7 +314,7 @@ export class LighterApiClient {
305
314
  path: string,
306
315
  authToken: string,
307
316
  params: ApiParams
308
- ): Promise<{ status: number; data: T }> {
317
+ ): Promise<{ status: number; data: T | undefined }> {
309
318
  const body = new URLSearchParams()
310
319
  for (const [k, v] of Object.entries(params)) {
311
320
  body.set(k, String(v))
@@ -320,7 +329,7 @@ export class LighterApiClient {
320
329
  signal: this.signal,
321
330
  })
322
331
  this.assertNotRateLimited(response)
323
- const data = (await response.json().catch(() => undefined)) as T
332
+ const data = (await response.json().catch(() => undefined)) as T | undefined
324
333
  return { status: response.status, data }
325
334
  }
326
335
 
@@ -344,13 +353,13 @@ export class LighterApiClient {
344
353
  if (status < 200 || status >= 300) {
345
354
  throw new PerpsError(
346
355
  code,
347
- `Lighter API request failed: ${status} — ${JSON.stringify(data).slice(0, 200)}`
356
+ `Lighter API request failed: ${status} — ${lighterBodyExcerpt(data)}`
348
357
  )
349
358
  }
350
359
  if (errorCode !== undefined) {
351
360
  throw new PerpsError(
352
361
  code,
353
- `Lighter API error for ${path}: code ${errorCode} — ${JSON.stringify(data).slice(0, 200)}`
362
+ `Lighter API error for ${path}: code ${errorCode} — ${lighterBodyExcerpt(data)}`
354
363
  )
355
364
  }
356
365
  }
@@ -3,7 +3,7 @@ import { PerpsErrorCode } from '@lifi/perps-types'
3
3
  import type { Address } from 'viem'
4
4
  import { LIGHTER_CODE_ACCOUNT_NOT_FOUND } from '../constants.js'
5
5
  import type { LtAccount, LtDetailedAccount } from '../types/index.js'
6
- import type { LighterApiClient } from './apiClient.js'
6
+ import { type LighterApiClient, lighterBodyExcerpt } from './apiClient.js'
7
7
  import { wireList } from './wireList.js'
8
8
 
9
9
  /**
@@ -33,7 +33,7 @@ export const fetchDetailedAccount = async (
33
33
  if (status < 200 || status >= 300) {
34
34
  throw new PerpsError(
35
35
  PerpsErrorCode.ThirdPartyError,
36
- `Lighter account request failed: ${status} — ${JSON.stringify(data).slice(0, 200)}`
36
+ `Lighter account request failed: ${status} — ${lighterBodyExcerpt(data)}`
37
37
  )
38
38
  }
39
39
 
@@ -6,12 +6,9 @@ import {
6
6
  import type { Fill, MarketDisplay } from '@lifi/perps-types'
7
7
  import { LiquidityRole, OrderSide, OrderType } from '@lifi/perps-types'
8
8
  import Big from 'big.js'
9
- import {
10
- LIGHTER_FEE_TICK_SCALE,
11
- LIGHTER_IMF_PERCENT_SCALE,
12
- } from '../constants.js'
9
+ import { LIGHTER_FEE_TICK_SCALE } from '../constants.js'
13
10
  import type { LtTrade } from '../types/index.js'
14
- import { leverageFromImf } from './mapPosition.js'
11
+ import { leverageFromScaledImf } from './mapPosition.js'
15
12
  import { isPlaceholderTxHash } from './txHash.js'
16
13
 
17
14
  /**
@@ -36,16 +33,12 @@ const tickToFeeAmount = (
36
33
  .toFixed()
37
34
 
38
35
  /**
39
- * Display leverage the viewer had set on the market when the trade executed.
40
- * Lighter reports it as the pre-trade initial margin fraction, an integer
41
- * percent on `LIGHTER_IMF_PERCENT_SCALE`: `500` is 5.00%, so 20x. Returns
42
- * `undefined` when the row omits the fraction. Lighter declares the fraction a
43
- * `StrictInt`, so the scale division is exact and `toFixed()` loses nothing.
36
+ * Display leverage the viewer had set on the market when the trade executed,
37
+ * from the pre-trade initial margin fraction. Returns `undefined` when the
38
+ * row omits the fraction.
44
39
  */
45
40
  const leverageFromTradeImf = (imf: number | undefined): number | undefined =>
46
- imf === undefined
47
- ? undefined
48
- : leverageFromImf(new Big(imf).div(LIGHTER_IMF_PERCENT_SCALE).toFixed())
41
+ imf === undefined ? undefined : leverageFromScaledImf(imf)
49
42
 
50
43
  /**
51
44
  * Realized PnL on a position-reducing fill, derived from the pre-trade entry
@@ -5,48 +5,63 @@ import type {
5
5
  import Big from 'big.js'
6
6
  import type { LtPnLEntry } from '../types/pnl.js'
7
7
 
8
+ function getCumulativePnl(snapshot: LtPnLEntry): Big {
9
+ return new Big(snapshot.trade_pnl).plus(snapshot.trade_spot_pnl)
10
+ }
11
+
12
+ function getCumulativeNetFlow(snapshot: LtPnLEntry): Big {
13
+ return new Big(snapshot.inflow)
14
+ .minus(snapshot.outflow)
15
+ .plus(snapshot.spot_inflow)
16
+ .minus(snapshot.spot_outflow)
17
+ }
18
+
8
19
  /**
9
- * Build the portfolio series from Lighter PnL buckets and the account's
10
- * current value. Lighter reports no historical account value, so each
11
- * point's `accountValue` is `currentValue` with every later bucket's
12
- * `trade_pnl + trade_spot_pnl + inflow - outflow` removed. `pnl` is the
13
- * running `trade_pnl + trade_spot_pnl`, so both series unwind the same PnL.
20
+ * Derive window-relative history from Lighter's cumulative PnL snapshots.
21
+ * Historical account values are anchored to the current account value.
14
22
  */
15
23
  export const mapPortfolioHistory = (
16
24
  range: PortfolioHistoryRange,
17
- entries: LtPnLEntry[],
25
+ snapshots: LtPnLEntry[],
18
26
  currentValue: Big
19
27
  ): PortfolioHistoryResponse => {
20
- const ordered = [...entries].sort((a, b) => a.timestamp - b.timestamp)
21
-
22
- const accountValues: Big[] = []
23
- let value = currentValue
24
- for (let i = ordered.length - 1; i >= 0; i -= 1) {
25
- accountValues[i] = value
26
- const entry = ordered[i]
27
- value = value
28
- .minus(new Big(entry.trade_pnl))
29
- .minus(new Big(entry.trade_spot_pnl))
30
- .minus(new Big(entry.inflow))
31
- .plus(new Big(entry.outflow))
28
+ const orderedSnapshots = [...snapshots].sort(
29
+ (a, b) => a.timestamp - b.timestamp
30
+ )
31
+ const firstSnapshot = orderedSnapshots[0]
32
+ const latestSnapshot = orderedSnapshots.at(-1)
33
+ if (!firstSnapshot || !latestSnapshot) {
34
+ return {
35
+ range,
36
+ points: [],
37
+ volume: '0',
38
+ totalPnl: undefined,
39
+ }
32
40
  }
33
41
 
34
- let pnl = new Big(0)
35
- let volume = new Big(0)
36
- const points = ordered.map((entry, i) => {
37
- pnl = pnl.plus(new Big(entry.trade_pnl)).plus(new Big(entry.trade_spot_pnl))
38
- volume = volume.plus(new Big(entry.volume))
42
+ const firstPnl = getCumulativePnl(firstSnapshot)
43
+ const latestPnl = getCumulativePnl(latestSnapshot)
44
+ const accountValueOffset = currentValue
45
+ .minus(latestPnl)
46
+ .minus(getCumulativeNetFlow(latestSnapshot))
47
+ const points = orderedSnapshots.map((snapshot) => {
48
+ const pnl = getCumulativePnl(snapshot)
39
49
  return {
40
- timestamp: entry.timestamp,
41
- accountValue: accountValues[i].toFixed(),
42
- pnl: pnl.toFixed(),
50
+ timestamp: snapshot.timestamp * 1_000,
51
+ accountValue: accountValueOffset
52
+ .plus(pnl)
53
+ .plus(getCumulativeNetFlow(snapshot))
54
+ .toFixed(),
55
+ pnl: pnl.minus(firstPnl).toFixed(),
43
56
  }
44
57
  })
45
58
 
46
59
  return {
47
60
  range,
48
61
  points,
49
- volume: volume.toFixed(),
62
+ volume: new Big(latestSnapshot.volume)
63
+ .minus(firstSnapshot.volume)
64
+ .toFixed(),
50
65
  totalPnl: points.at(-1)?.pnl,
51
66
  }
52
67
  }
@@ -1,6 +1,7 @@
1
1
  import type { PerpsMarketDisplay, Position } from '@lifi/perps-types'
2
2
  import { MarginMode, PositionSide } from '@lifi/perps-types'
3
3
  import Big from 'big.js'
4
+ import { LIGHTER_IMF_PERCENT_SCALE } from '../constants.js'
4
5
  import type { LtAccountPosition } from '../types/index.js'
5
6
  import { LT_MARGIN_MODE_ISOLATED } from '../types/index.js'
6
7
  import { toPositiveRequiredBig, toRequiredBig } from './decimal.js'
@@ -25,6 +26,22 @@ export const leverageFromImf = (imf: string): number | undefined => {
25
26
  return new Big(100).div(parsed).toNumber()
26
27
  }
27
28
 
29
+ /**
30
+ * Display leverage from an integer IMF on `LIGHTER_IMF_PERCENT_SCALE`, the
31
+ * unit of trade rows and order-book details: `500` is 5.00%, so 20x.
32
+ * `undefined` for a non-positive or unparsable IMF. Lighter declares the
33
+ * fraction a `StrictInt`, so the scale division is exact.
34
+ */
35
+ export const leverageFromScaledImf = (imf: number): number | undefined => {
36
+ let percent: string
37
+ try {
38
+ percent = new Big(imf).div(LIGHTER_IMF_PERCENT_SCALE).toFixed()
39
+ } catch {
40
+ return undefined
41
+ }
42
+ return leverageFromImf(percent)
43
+ }
44
+
28
45
  /**
29
46
  * Map a raw Lighter account position to the generic Position type.
30
47
  * @param market - Backend-resolved market identity for `pos.market_id`.
@@ -5,9 +5,11 @@ import { PerpsErrorCode } from '@lifi/perps-types'
5
5
  /**
6
6
  * Map our `OhlcvInterval` literal to Lighter's `resolution` enum.
7
7
  *
8
- * Lighter exposes only the timeframes listed below. SDK intervals without a
9
- * direct match (3m, 2h, 8h, 3d, 1w, 1M) raise a validation error rather than
10
- * silently rounding — the caller picks a supported timeframe.
8
+ * The keys are exactly the `resolution` enum of Lighter's `GET /api/v1/candles`.
9
+ * Lighter has no weekly candle (its API answers `1w` with code 20001,
10
+ * `invalid param`), so `1w` stays out. SDK intervals without a direct match
11
+ * (3m, 2h, 8h, 3d, 1w, 1M) raise a validation error rather than silently
12
+ * rounding — the caller picks a supported timeframe.
11
13
  */
12
14
  const LIGHTER_SUPPORTED_INTERVALS: Partial<Record<OhlcvInterval, string>> = {
13
15
  '1m': '1m',
@@ -1,5 +1,5 @@
1
1
  import type { Market, MarketContext } from '@lifi/perps-types'
2
- import type Big from 'big.js'
2
+ import Big from 'big.js'
3
3
  import { toRequiredBig } from './decimal.js'
4
4
 
5
5
  /**
@@ -37,3 +37,23 @@ export const spotPriceByAssetId = (
37
37
  }
38
38
  return priceByAssetId
39
39
  }
40
+
41
+ /**
42
+ * USD value of `units` of a Lighter asset, with the unit price it used: the
43
+ * settlement asset at 1, any other at its `spotPrices` entry. An asset absent
44
+ * from `spotPrices` values at 0 and carries no price.
45
+ */
46
+ export const spotValuation = (
47
+ assetId: number,
48
+ units: string,
49
+ settlementAssetIndex: number,
50
+ spotPrices: ReadonlyMap<string, Big>
51
+ ): { valueUsd: string; price?: Big } => {
52
+ const price =
53
+ assetId === settlementAssetIndex
54
+ ? new Big(1)
55
+ : spotPrices.get(String(assetId))
56
+ return price === undefined
57
+ ? { valueUsd: '0' }
58
+ : { valueUsd: new Big(units).times(price).toFixed(), price }
59
+ }
@@ -1,20 +1,29 @@
1
- import type { ProviderWithdrawableBalance } from '@lifi/perps-sdk'
2
- import type { LtAccountAsset } from '../types/account.js'
1
+ import { maxOf, minOf, type ProviderWithdrawableBalance } from '@lifi/perps-sdk'
2
+ import Big from 'big.js'
3
+ import type { LtAccount } from '../types/account.js'
3
4
  import { toRequiredBig } from './decimal.js'
4
5
 
5
6
  /**
6
7
  * Split each held asset into the two routes a Lighter withdrawal can name:
7
8
  * spot draws on `balance` net of `locked_balance`, perps draws on
8
- * `margin_balance`. Routes with nothing left to draw are dropped; the
9
- * per-asset venue minimum is applied by the caller holding the asset registry.
9
+ * `margin_balance`. The settlement asset's perps route is also capped by the
10
+ * account `available_balance`, which excludes the margin open positions use.
11
+ * Every other asset's perps route reports its full `margin_balance`, uncapped,
12
+ * although `getAccount` reports `transferable: '0'` for it: a category
13
+ * transfer moves only the settlement asset. Routes with nothing left to draw
14
+ * are dropped; the per-asset venue minimum is applied by the caller holding
15
+ * the asset registry.
10
16
  *
17
+ * @param settlementAssetIndex - L2 asset index of the deployment's collateral
18
+ * asset, the only asset that `available_balance` is denominated in.
11
19
  * @public
12
20
  */
13
21
  export const lighterWithdrawableBalances = (
14
- assets: LtAccountAsset[]
22
+ account: Pick<LtAccount, 'assets' | 'available_balance'>,
23
+ settlementAssetIndex: number
15
24
  ): ProviderWithdrawableBalance[] => {
16
25
  const rows: ProviderWithdrawableBalance[] = []
17
- for (const asset of assets) {
26
+ for (const asset of account.assets) {
18
27
  const assetId = String(asset.asset_id)
19
28
  const spot = toRequiredBig(asset.balance, 'balance').minus(
20
29
  toRequiredBig(asset.locked_balance, 'locked_balance')
@@ -22,7 +31,17 @@ export const lighterWithdrawableBalances = (
22
31
  if (spot.gt(0)) {
23
32
  rows.push({ assetId, route: 'spot', available: spot.toFixed() })
24
33
  }
25
- const perps = toRequiredBig(asset.margin_balance, 'margin_balance')
34
+ const marginBalance = toRequiredBig(asset.margin_balance, 'margin_balance')
35
+ const perps =
36
+ asset.asset_id === settlementAssetIndex
37
+ ? minOf(
38
+ maxOf(
39
+ toRequiredBig(account.available_balance, 'available_balance'),
40
+ new Big(0)
41
+ ),
42
+ marginBalance
43
+ )
44
+ : marginBalance
26
45
  if (perps.gt(0)) {
27
46
  rows.push({ assetId, route: 'perps', available: perps.toFixed() })
28
47
  }