@lifi/perps-sdk-provider-lighter 26.0.0 → 28.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 (68) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +26 -12
  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 +17 -20
  6. package/dist/cjs/accountSummary.js.map +1 -1
  7. package/dist/cjs/utils/index.d.ts +1 -1
  8. package/dist/cjs/utils/index.d.ts.map +1 -1
  9. package/dist/cjs/utils/index.js +2 -2
  10. package/dist/cjs/utils/index.js.map +1 -1
  11. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  12. package/dist/cjs/utils/mapOrder.js +2 -1
  13. package/dist/cjs/utils/mapOrder.js.map +1 -1
  14. package/dist/cjs/utils/spotPrice.d.ts +4 -0
  15. package/dist/cjs/utils/spotPrice.d.ts.map +1 -0
  16. package/dist/cjs/utils/spotPrice.js +24 -0
  17. package/dist/cjs/utils/spotPrice.js.map +1 -0
  18. package/dist/cjs/utils/transferMargin.d.ts +2 -2
  19. package/dist/cjs/utils/transferMargin.d.ts.map +1 -1
  20. package/dist/cjs/utils/transferMargin.js +33 -8
  21. package/dist/cjs/utils/transferMargin.js.map +1 -1
  22. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  23. package/dist/cjs/websocket/LighterWsProvider.js +1 -0
  24. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  25. package/dist/esm/LighterProvider.d.ts.map +1 -1
  26. package/dist/esm/LighterProvider.js +28 -21
  27. package/dist/esm/LighterProvider.js.map +1 -1
  28. package/dist/esm/accountSummary.d.ts +6 -6
  29. package/dist/esm/accountSummary.d.ts.map +1 -1
  30. package/dist/esm/accountSummary.js +24 -27
  31. package/dist/esm/accountSummary.js.map +1 -1
  32. package/dist/esm/utils/index.d.ts +1 -1
  33. package/dist/esm/utils/index.d.ts.map +1 -1
  34. package/dist/esm/utils/index.js +1 -1
  35. package/dist/esm/utils/index.js.map +1 -1
  36. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  37. package/dist/esm/utils/mapOrder.js +2 -1
  38. package/dist/esm/utils/mapOrder.js.map +1 -1
  39. package/dist/esm/utils/spotPrice.d.ts +14 -0
  40. package/dist/esm/utils/spotPrice.d.ts.map +1 -0
  41. package/dist/esm/utils/spotPrice.js +30 -0
  42. package/dist/esm/utils/spotPrice.js.map +1 -0
  43. package/dist/esm/utils/transferMargin.d.ts +11 -5
  44. package/dist/esm/utils/transferMargin.d.ts.map +1 -1
  45. package/dist/esm/utils/transferMargin.js +39 -11
  46. package/dist/esm/utils/transferMargin.js.map +1 -1
  47. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  48. package/dist/esm/websocket/LighterWsProvider.js +1 -0
  49. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  50. package/dist/types/LighterProvider.d.ts.map +1 -1
  51. package/dist/types/accountSummary.d.ts +6 -6
  52. package/dist/types/accountSummary.d.ts.map +1 -1
  53. package/dist/types/utils/index.d.ts +1 -1
  54. package/dist/types/utils/index.d.ts.map +1 -1
  55. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  56. package/dist/types/utils/spotPrice.d.ts +14 -0
  57. package/dist/types/utils/spotPrice.d.ts.map +1 -0
  58. package/dist/types/utils/transferMargin.d.ts +11 -5
  59. package/dist/types/utils/transferMargin.d.ts.map +1 -1
  60. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  61. package/package.json +3 -3
  62. package/src/LighterProvider.ts +50 -36
  63. package/src/accountSummary.ts +35 -28
  64. package/src/utils/index.ts +1 -1
  65. package/src/utils/mapOrder.ts +2 -1
  66. package/src/utils/spotPrice.ts +39 -0
  67. package/src/utils/transferMargin.ts +52 -14
  68. package/src/websocket/LighterWsProvider.ts +1 -0
@@ -1,54 +1,61 @@
1
+ import { PerpsError } from '@lifi/perps-sdk'
1
2
  import type {
2
3
  AccountResponse,
3
4
  AccountSummary,
5
+ LighterAccountConfig,
4
6
  Position,
5
7
  } from '@lifi/perps-types'
6
- import { MarginMode } from '@lifi/perps-types'
8
+ import { PerpsErrorCode } from '@lifi/perps-types'
7
9
  import Big from 'big.js'
10
+ import { toRequiredBig } from './utils/decimal.js'
11
+
12
+ const lighterConfig = (account: AccountResponse): LighterAccountConfig => {
13
+ if (account.config.provider !== 'lighter') {
14
+ throw new PerpsError(
15
+ PerpsErrorCode.SDKError,
16
+ `Lighter account summary received a '${account.config.provider}' account config`
17
+ )
18
+ }
19
+ return account.config
20
+ }
8
21
 
9
22
  /**
10
- * Roll a Lighter {@link AccountResponse} up into an {@link AccountSummary}.
11
- *
12
- * Lighter's cross available balance is already marked to market, while an
13
- * isolated position's unrealized PnL remains inside that position's isolated
14
- * sub-account. Reconciliation therefore adds every position's locked margin,
15
- * but adds unrealized PnL only for isolated positions.
23
+ * Roll a Lighter account up into an {@link AccountSummary}. `totalAssetValue`
24
+ * is perps-route equity and excludes the spot route, so `portfolioValue` adds
25
+ * every spot `balances` row, settlement included; the `collateralBalances` row
26
+ * is buying power already inside `totalAssetValue`. The positions supply only
27
+ * the margin and PnL breakdown.
16
28
  *
29
+ * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
17
30
  * @public
18
31
  */
19
32
  export function getAccountSummary(
20
33
  account: AccountResponse,
21
34
  positions: Position[]
22
35
  ): AccountSummary {
23
- let availableMargin = new Big(0)
24
- for (const balance of account.collateralBalances) {
25
- availableMargin = availableMargin.plus(balance.valueUsd)
26
- }
27
-
28
- let nonCollateralValue = new Big(0)
29
- for (const balance of account.balances) {
30
- nonCollateralValue = nonCollateralValue.plus(balance.valueUsd)
31
- }
36
+ const config = lighterConfig(account)
32
37
 
33
38
  let marginUsed = new Big(0)
34
39
  let unrealizedPnl = new Big(0)
35
- let isolatedUnrealizedPnl = new Big(0)
36
40
  for (const position of positions) {
37
41
  marginUsed = marginUsed.plus(position.marginUsed)
38
42
  unrealizedPnl = unrealizedPnl.plus(position.unrealizedPnl)
39
- if (position.marginMode === MarginMode.ISOLATED) {
40
- isolatedUnrealizedPnl = isolatedUnrealizedPnl.plus(position.unrealizedPnl)
41
- }
43
+ }
44
+
45
+ let portfolioValue = toRequiredBig(config.totalAssetValue, 'totalAssetValue')
46
+ for (const balance of account.balances) {
47
+ portfolioValue = portfolioValue.plus(
48
+ toRequiredBig(balance.valueUsd, 'valueUsd')
49
+ )
42
50
  }
43
51
 
44
52
  return {
45
- portfolioValue: nonCollateralValue
46
- .plus(availableMargin)
47
- .plus(marginUsed)
48
- .plus(isolatedUnrealizedPnl)
49
- .toString(),
50
- availableMargin: availableMargin.toString(),
51
- marginUsed: marginUsed.toString(),
52
- unrealizedPnl: unrealizedPnl.toString(),
53
+ portfolioValue: portfolioValue.toFixed(),
54
+ availableMargin: toRequiredBig(
55
+ config.availableBalance,
56
+ 'availableBalance'
57
+ ).toFixed(),
58
+ marginUsed: marginUsed.toFixed(),
59
+ unrealizedPnl: unrealizedPnl.toFixed(),
53
60
  }
54
61
  }
@@ -23,7 +23,7 @@ export { mapMarketOrderLimits } from './marketLimits.js'
23
23
  export { mapInterval } from './ohlcvInterval.js'
24
24
  export { formatOrderPrice, formatOrderSize } from './orderFormatting.js'
25
25
  export { toIsoFromMs, toIsoFromSeconds } from './time.js'
26
- export { positionMarginConstraints } from './transferMargin.js'
26
+ export { positionRemovableMargin } from './transferMargin.js'
27
27
  export {
28
28
  leverageToFraction,
29
29
  mapOrderTypeToInt,
@@ -61,9 +61,10 @@ const mapOrderStatus = (
61
61
  filledSize = new Big(order.filled_base_amount)
62
62
  ): OrderStatus => {
63
63
  switch (order.status) {
64
- case 'pending':
65
64
  case 'in-progress':
66
65
  return OrderStatus.ACCEPTED
66
+ case 'pending':
67
+ return OrderStatus.OPEN
67
68
  case 'open':
68
69
  if (order.trigger_status === 'parent-order') {
69
70
  return OrderStatus.PENDING
@@ -0,0 +1,39 @@
1
+ import type { Market, MarketContext } from '@lifi/perps-types'
2
+ import type Big from 'big.js'
3
+ import { toRequiredBig } from './decimal.js'
4
+
5
+ /**
6
+ * Unit prices keyed by spot base `Asset.id` (the venue `asset_id`), read from
7
+ * each spot market's `markPrice` for the base assets in `assetIds` only. A
8
+ * market without a context row or with a non-positive mark adds no entry; when
9
+ * several spot markets share a base asset, the first priced one in `markets`
10
+ * order wins. The mark is read as a USD price; `quoteAsset` is not checked.
11
+ *
12
+ * @throws {PerpsError} `SDKError` when a context row for a requested asset
13
+ * carries a non-decimal mark.
14
+ */
15
+ export const spotPriceByAssetId = (
16
+ markets: readonly Market[],
17
+ spotCategoryId: string,
18
+ contexts: readonly MarketContext[],
19
+ assetIds: ReadonlySet<string>
20
+ ): Map<string, Big> => {
21
+ const markByMarketId = new Map(contexts.map((c) => [c.marketId, c.markPrice]))
22
+ const priceByAssetId = new Map<string, Big>()
23
+ for (const market of markets) {
24
+ const mark = markByMarketId.get(market.id)
25
+ if (
26
+ market.categoryId !== spotCategoryId ||
27
+ mark === undefined ||
28
+ !assetIds.has(market.baseAsset.id) ||
29
+ priceByAssetId.has(market.baseAsset.id)
30
+ ) {
31
+ continue
32
+ }
33
+ const price = toRequiredBig(mark, 'markPrice')
34
+ if (price.gt(0)) {
35
+ priceByAssetId.set(market.baseAsset.id, price)
36
+ }
37
+ }
38
+ return priceByAssetId
39
+ }
@@ -1,31 +1,69 @@
1
+ import { PerpsError } from '@lifi/perps-sdk'
1
2
  import {
3
+ PerpsErrorCode,
2
4
  type Position,
3
- type PositionMarginConstraints,
4
5
  positionSupportsMarginAdjustment,
6
+ positionSupportsMarginRemoval,
5
7
  } from '@lifi/perps-types'
6
- import { toPositiveRequiredBig } from './decimal.js'
8
+ import Big from 'big.js'
7
9
 
8
- const AMOUNT_INCREMENT = '0.000001'
10
+ const AMOUNT_DECIMALS = 6
11
+
12
+ function positionAmount(value: string, field: string): Big {
13
+ try {
14
+ return new Big(value)
15
+ } catch {
16
+ throw new PerpsError(
17
+ PerpsErrorCode.ValidationError,
18
+ `Invalid decimal string on Position.${field}: '${value}'`
19
+ )
20
+ }
21
+ }
22
+
23
+ function positivePositionAmount(value: string, field: string): Big {
24
+ const amount = positionAmount(value, field)
25
+ if (amount.lte(0)) {
26
+ throw new PerpsError(
27
+ PerpsErrorCode.ValidationError,
28
+ `Position.${field} must be greater than zero.`
29
+ )
30
+ }
31
+ return amount
32
+ }
9
33
 
10
34
  /**
11
- * Lighter's exact isolated-margin requirement. Lighter publishes no separate
12
- * notional floor, so the provider-normalized initial-margin requirement is
13
- * the retained amount.
35
+ * Lighter removable isolated margin:
36
+ * `allocated_margin + unrealized PnL − initial margin requirement`. Lighter
37
+ * `allocated_margin` (`Position.marginUsed`) excludes the unrealized PnL, and
38
+ * Lighter publishes no separate notional floor.
14
39
  *
40
+ * @returns `undefined` for cross positions and markets without individual
41
+ * margin adjustment; `'0'` for add-only markets and when nothing is
42
+ * removable.
43
+ * @throws {PerpsError} `ValidationError` when a required `Position` decimal
44
+ * is malformed or not positive.
15
45
  * @see https://docs.lighter.xyz/trading/liquidations-and-llp-insurance-fund
16
46
  * @public
17
47
  */
18
- export function positionMarginConstraints(
48
+ export function positionRemovableMargin(
19
49
  position: Position
20
- ): PositionMarginConstraints | undefined {
50
+ ): string | undefined {
21
51
  if (!positionSupportsMarginAdjustment(position)) {
22
52
  return undefined
23
53
  }
24
- return {
25
- minimumMarginRequirement: toPositiveRequiredBig(
26
- position.initialMarginRequirement,
27
- 'initialMarginRequirement'
28
- ).toFixed(),
29
- amountIncrement: AMOUNT_INCREMENT,
54
+ if (!positionSupportsMarginRemoval(position)) {
55
+ return '0'
56
+ }
57
+ const removable = positivePositionAmount(position.marginUsed, 'marginUsed')
58
+ .plus(positionAmount(position.unrealizedPnl, 'unrealizedPnl'))
59
+ .minus(
60
+ positivePositionAmount(
61
+ position.initialMarginRequirement,
62
+ 'initialMarginRequirement'
63
+ )
64
+ )
65
+ if (removable.lte(0)) {
66
+ return '0'
30
67
  }
68
+ return removable.round(AMOUNT_DECIMALS, Big.roundDown).toFixed()
31
69
  }
@@ -374,6 +374,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
374
374
  return `accountSummary:${sub.address.toLowerCase()}`
375
375
  case 'candle':
376
376
  case 'spotBalances':
377
+ case 'availableToTrade':
377
378
  throw new Error(`Lighter WS does not support channel: ${sub.channel}.`)
378
379
  }
379
380
  }