@lifi/perps-sdk-provider-lighter 7.0.0 → 8.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 (94) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +60 -27
  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 +33 -2
  6. package/dist/cjs/accountSummary.js.map +1 -1
  7. package/dist/cjs/types/account.d.ts +1 -0
  8. package/dist/cjs/types/account.d.ts.map +1 -1
  9. package/dist/cjs/types/ws.d.ts +5 -0
  10. package/dist/cjs/types/ws.d.ts.map +1 -1
  11. package/dist/cjs/utils/decimal.d.ts +5 -0
  12. package/dist/cjs/utils/decimal.d.ts.map +1 -0
  13. package/dist/cjs/utils/decimal.js +38 -0
  14. package/dist/cjs/utils/decimal.js.map +1 -0
  15. package/dist/cjs/utils/index.d.ts +3 -1
  16. package/dist/cjs/utils/index.d.ts.map +1 -1
  17. package/dist/cjs/utils/index.js +7 -1
  18. package/dist/cjs/utils/index.js.map +1 -1
  19. package/dist/cjs/utils/mapOpenPositions.d.ts +2 -2
  20. package/dist/cjs/utils/mapOpenPositions.d.ts.map +1 -1
  21. package/dist/cjs/utils/mapOpenPositions.js.map +1 -1
  22. package/dist/cjs/utils/mapPosition.d.ts +3 -2
  23. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  24. package/dist/cjs/utils/mapPosition.js +30 -9
  25. package/dist/cjs/utils/mapPosition.js.map +1 -1
  26. package/dist/cjs/utils/transferMargin.d.ts +3 -0
  27. package/dist/cjs/utils/transferMargin.d.ts.map +1 -0
  28. package/dist/cjs/utils/transferMargin.js +17 -0
  29. package/dist/cjs/utils/transferMargin.js.map +1 -0
  30. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  31. package/dist/cjs/websocket/LighterWsProvider.js +20 -9
  32. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  33. package/dist/esm/LighterProvider.d.ts.map +1 -1
  34. package/dist/esm/LighterProvider.js +49 -11
  35. package/dist/esm/LighterProvider.js.map +1 -1
  36. package/dist/esm/accountSummary.d.ts +5 -4
  37. package/dist/esm/accountSummary.d.ts.map +1 -1
  38. package/dist/esm/accountSummary.js +35 -6
  39. package/dist/esm/accountSummary.js.map +1 -1
  40. package/dist/esm/types/account.d.ts +3 -0
  41. package/dist/esm/types/account.d.ts.map +1 -1
  42. package/dist/esm/types/ws.d.ts +9 -1
  43. package/dist/esm/types/ws.d.ts.map +1 -1
  44. package/dist/esm/utils/decimal.d.ts +15 -0
  45. package/dist/esm/utils/decimal.d.ts.map +1 -0
  46. package/dist/esm/utils/decimal.js +39 -0
  47. package/dist/esm/utils/decimal.js.map +1 -0
  48. package/dist/esm/utils/index.d.ts +3 -1
  49. package/dist/esm/utils/index.d.ts.map +1 -1
  50. package/dist/esm/utils/index.js +3 -1
  51. package/dist/esm/utils/index.js.map +1 -1
  52. package/dist/esm/utils/mapOpenPositions.d.ts +2 -2
  53. package/dist/esm/utils/mapOpenPositions.d.ts.map +1 -1
  54. package/dist/esm/utils/mapOpenPositions.js.map +1 -1
  55. package/dist/esm/utils/mapPosition.d.ts +10 -2
  56. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  57. package/dist/esm/utils/mapPosition.js +32 -11
  58. package/dist/esm/utils/mapPosition.js.map +1 -1
  59. package/dist/esm/utils/transferMargin.d.ts +11 -0
  60. package/dist/esm/utils/transferMargin.d.ts.map +1 -0
  61. package/dist/esm/utils/transferMargin.js +22 -0
  62. package/dist/esm/utils/transferMargin.js.map +1 -0
  63. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  64. package/dist/esm/websocket/LighterWsProvider.js +24 -13
  65. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  66. package/dist/types/LighterProvider.d.ts.map +1 -1
  67. package/dist/types/accountSummary.d.ts +5 -4
  68. package/dist/types/accountSummary.d.ts.map +1 -1
  69. package/dist/types/types/account.d.ts +3 -0
  70. package/dist/types/types/account.d.ts.map +1 -1
  71. package/dist/types/types/ws.d.ts +9 -1
  72. package/dist/types/types/ws.d.ts.map +1 -1
  73. package/dist/types/utils/decimal.d.ts +15 -0
  74. package/dist/types/utils/decimal.d.ts.map +1 -0
  75. package/dist/types/utils/index.d.ts +3 -1
  76. package/dist/types/utils/index.d.ts.map +1 -1
  77. package/dist/types/utils/mapOpenPositions.d.ts +2 -2
  78. package/dist/types/utils/mapOpenPositions.d.ts.map +1 -1
  79. package/dist/types/utils/mapPosition.d.ts +10 -2
  80. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  81. package/dist/types/utils/transferMargin.d.ts +11 -0
  82. package/dist/types/utils/transferMargin.d.ts.map +1 -0
  83. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  84. package/package.json +3 -3
  85. package/src/LighterProvider.ts +75 -9
  86. package/src/accountSummary.ts +38 -6
  87. package/src/types/account.ts +3 -0
  88. package/src/types/ws.ts +9 -1
  89. package/src/utils/decimal.ts +52 -0
  90. package/src/utils/index.ts +3 -1
  91. package/src/utils/mapOpenPositions.ts +2 -2
  92. package/src/utils/mapPosition.ts +40 -14
  93. package/src/utils/transferMargin.ts +35 -0
  94. package/src/websocket/LighterWsProvider.ts +41 -11
@@ -0,0 +1,52 @@
1
+ import { PerpsError } from '@lifi/perps-sdk'
2
+ import { PerpsErrorCode } from '@lifi/perps-types'
3
+ import Big from 'big.js'
4
+
5
+ /**
6
+ * Exact-decimal parse of a venue decimal string; null when absent or
7
+ * unparsable, so callers can gate on presence before deriving.
8
+ * @public
9
+ */
10
+ export const toBigOrNull = (value: string | undefined): Big | null => {
11
+ if (value === undefined) {
12
+ return null
13
+ }
14
+ try {
15
+ return new Big(value)
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Parse a required venue decimal and identify the bad field on failure.
23
+ * @public
24
+ */
25
+ export const toRequiredBig = (
26
+ value: string | undefined,
27
+ field: string
28
+ ): Big => {
29
+ const parsed = toBigOrNull(value)
30
+ if (parsed !== null) {
31
+ return parsed
32
+ }
33
+ throw new PerpsError(
34
+ PerpsErrorCode.SDKError,
35
+ `Lighter field \`${field}\` is not a valid decimal.`
36
+ )
37
+ }
38
+
39
+ /** Parse a required venue decimal that must be greater than zero. */
40
+ export const toPositiveRequiredBig = (
41
+ value: string | undefined,
42
+ field: string
43
+ ): Big => {
44
+ const parsed = toRequiredBig(value, field)
45
+ if (parsed.lte(0)) {
46
+ throw new PerpsError(
47
+ PerpsErrorCode.SDKError,
48
+ `Lighter field \`${field}\` must be greater than zero.`
49
+ )
50
+ }
51
+ return parsed
52
+ }
@@ -9,6 +9,7 @@ export {
9
9
  } from './activityCursor.js'
10
10
  export { LighterApiClient } from './apiClient.js'
11
11
  export { assetMarginModeInt, isAssetMarginEnabled } from './assetCollateral.js'
12
+ export { toBigOrNull, toRequiredBig } from './decimal.js'
12
13
  export { fetchDetailedAccount } from './fetchDetailedAccount.js'
13
14
  export { lighterAsset } from './lighterAsset.js'
14
15
  export { estimateLiquidationPrice } from './liquidation.js'
@@ -24,10 +25,11 @@ export {
24
25
  mapStatusReason,
25
26
  mapTriggerOrder,
26
27
  } from './mapOrder.js'
27
- export { mapPosition } from './mapPosition.js'
28
+ export { leverageFromImf, mapPosition } from './mapPosition.js'
28
29
  export { mapInterval } from './ohlcvInterval.js'
29
30
  export { formatOrderPrice, formatOrderSize } from './orderFormatting.js'
30
31
  export { toIsoFromMs, toIsoFromSeconds } from './time.js'
32
+ export { positionMarginConstraints } from './transferMargin.js'
31
33
  export {
32
34
  leverageToFraction,
33
35
  mapOrderTypeToInt,
@@ -1,4 +1,4 @@
1
- import type { MarketDisplay, Position } from '@lifi/perps-types'
1
+ import type { PerpsMarketDisplay, Position } from '@lifi/perps-types'
2
2
  import type { LtAccountPosition } from '../types/index.js'
3
3
  import { mapPosition } from './mapPosition.js'
4
4
 
@@ -11,7 +11,7 @@ import { mapPosition } from './mapPosition.js'
11
11
  */
12
12
  export const mapOpenPositions = (
13
13
  positions: LtAccountPosition[],
14
- resolveMarket: (marketId: number) => MarketDisplay
14
+ resolveMarket: (marketId: number) => PerpsMarketDisplay
15
15
  ): Position[] =>
16
16
  positions
17
17
  .filter((p) => Number.parseFloat(p.position) !== 0)
@@ -1,7 +1,29 @@
1
- import type { MarketDisplay, Position } from '@lifi/perps-types'
1
+ import type { PerpsMarketDisplay, Position } from '@lifi/perps-types'
2
2
  import { MarginMode, PositionSide } from '@lifi/perps-types'
3
+ import Big from 'big.js'
3
4
  import type { LtAccountPosition } from '../types/index.js'
4
5
  import { LT_MARGIN_MODE_ISOLATED } from '../types/index.js'
6
+ import { toPositiveRequiredBig, toRequiredBig } from './decimal.js'
7
+
8
+ /**
9
+ * Display leverage from an IMF percent string: `100 / IMF` in exact decimal
10
+ * arithmetic before conversion to `number`. This value is for displaying the
11
+ * venue setting; provider risk calculations consume the original decimal IMF
12
+ * instead. `undefined` for a non-positive or unparsable IMF.
13
+ * @public
14
+ */
15
+ export const leverageFromImf = (imf: string): number | undefined => {
16
+ let parsed: Big
17
+ try {
18
+ parsed = new Big(imf)
19
+ } catch {
20
+ return undefined
21
+ }
22
+ if (parsed.lte(0)) {
23
+ return undefined
24
+ }
25
+ return new Big(100).div(parsed).toNumber()
26
+ }
5
27
 
6
28
  /**
7
29
  * Map a raw Lighter account position to the generic Position type.
@@ -10,33 +32,37 @@ import { LT_MARGIN_MODE_ISOLATED } from '../types/index.js'
10
32
  */
11
33
  export const mapPosition = (
12
34
  pos: LtAccountPosition,
13
- market: MarketDisplay
35
+ market: PerpsMarketDisplay
14
36
  ): Position => {
15
- const size = parseFloat(pos.position)
37
+ const size = toRequiredBig(pos.position, 'position')
16
38
  const isIsolated = pos.margin_mode === LT_MARGIN_MODE_ISOLATED
17
-
18
- // `allocated_margin` is only populated for isolated positions (always "0"
19
- // on cross accounts). For cross, derive margin as
20
- // `position_value × initial_margin_fraction / 100` (IMF is in percent).
21
- const positionValue = Math.abs(parseFloat(pos.position_value))
22
- const imf = parseFloat(pos.initial_margin_fraction)
39
+ const positionValue = toRequiredBig(
40
+ pos.position_value,
41
+ 'position_value'
42
+ ).abs()
43
+ const imf = toPositiveRequiredBig(
44
+ pos.initial_margin_fraction,
45
+ 'initial_margin_fraction'
46
+ )
47
+ const initialMarginRequirement = positionValue.times(imf).div(100)
23
48
  const marginUsed = isIsolated
24
49
  ? pos.allocated_margin
25
- : ((positionValue * imf) / 100).toString()
50
+ : initialMarginRequirement.toFixed()
26
51
 
27
52
  return {
28
53
  market,
29
54
  side: pos.sign >= 0 ? PositionSide.LONG : PositionSide.SHORT,
30
- size: Math.abs(size).toString(),
55
+ size: size.abs().toFixed(),
31
56
  entryPrice: pos.avg_entry_price,
32
57
  markPrice:
33
- pos.position_value === '0' || size === 0
58
+ positionValue.eq(0) || size.eq(0)
34
59
  ? '0'
35
- : (parseFloat(pos.position_value) / Math.abs(size)).toString(),
60
+ : positionValue.div(size.abs()).toFixed(),
36
61
  liquidationPrice: pos.liquidation_price,
37
62
  unrealizedPnl: pos.unrealized_pnl,
38
- leverage: imf > 0 ? Math.round(100 / imf) : 1,
63
+ leverage: leverageFromImf(pos.initial_margin_fraction) ?? 1,
39
64
  marginUsed,
65
+ initialMarginRequirement: initialMarginRequirement.toFixed(),
40
66
  marginMode: isIsolated ? MarginMode.ISOLATED : MarginMode.CROSS,
41
67
  }
42
68
  }
@@ -0,0 +1,35 @@
1
+ import {
2
+ MarginMode,
3
+ type Position,
4
+ PositionMarginAdjustment,
5
+ type PositionMarginConstraints,
6
+ } from '@lifi/perps-types'
7
+ import { toPositiveRequiredBig } from './decimal.js'
8
+
9
+ const AMOUNT_INCREMENT = '0.000001'
10
+
11
+ /**
12
+ * Lighter's exact isolated-margin requirement. Lighter publishes no separate
13
+ * notional floor, so the provider-normalized initial-margin requirement is
14
+ * the retained amount.
15
+ *
16
+ * @see https://docs.lighter.xyz/trading/liquidations-and-llp-insurance-fund
17
+ * @public
18
+ */
19
+ export function positionMarginConstraints(
20
+ position: Position
21
+ ): PositionMarginConstraints | undefined {
22
+ if (
23
+ position.marginMode !== MarginMode.ISOLATED ||
24
+ position.market.positionMarginAdjustment === PositionMarginAdjustment.NONE
25
+ ) {
26
+ return undefined
27
+ }
28
+ return {
29
+ minimumMarginRequirement: toPositiveRequiredBig(
30
+ position.initialMarginRequirement,
31
+ 'initialMarginRequirement'
32
+ ).toFixed(),
33
+ amountIncrement: AMOUNT_INCREMENT,
34
+ }
35
+ }
@@ -9,6 +9,7 @@ import {
9
9
  ReconnectingWebSocket,
10
10
  resolveRetryPolicy,
11
11
  resolveSubscribeQuote,
12
+ toPerpsMarketDisplay,
12
13
  WsProviderBase,
13
14
  type WsProviderFactory,
14
15
  wsLog,
@@ -51,6 +52,7 @@ import {
51
52
  mapFill,
52
53
  mapMarketContext,
53
54
  mapPosition,
55
+ toRequiredBig,
54
56
  } from '../utils/index.js'
55
57
 
56
58
  // Public channels: `marketsContext` (market_stats/all + spot_market_stats/all),
@@ -588,23 +590,51 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
588
590
  return
589
591
  }
590
592
  const stats = msg.stats
591
- const portfolio = Number.parseFloat(stats?.portfolio_value ?? '')
592
- const available = Number.parseFloat(stats?.available_balance ?? '')
593
- if (!Number.isFinite(portfolio) || !Number.isFinite(available)) {
593
+ if (stats === undefined) {
594
594
  return
595
595
  }
596
- // Identities of the REST 'net' semantics: `available_balance` nets locked
597
- // margin out with PnL marked in, and `collateral` is gross, PnL-exclusive.
598
- const collateral = Number.parseFloat(stats?.collateral ?? '')
596
+
597
+ const portfolio = toRequiredBig(
598
+ stats.portfolio_value,
599
+ 'stats.portfolio_value'
600
+ )
601
+ const collateral = toRequiredBig(stats.collateral, 'stats.collateral')
602
+
603
+ let available: Big
604
+ let marginUsed: Big
605
+ if (stats.cross_stats === undefined) {
606
+ // Compatibility with legacy gateways that omitted the entire
607
+ // cross_stats object.
608
+ available = toRequiredBig(
609
+ stats.available_balance,
610
+ 'stats.available_balance'
611
+ )
612
+ marginUsed = portfolio.minus(available)
613
+ } else {
614
+ const crossCollateral = toRequiredBig(
615
+ stats.cross_stats.collateral,
616
+ 'stats.cross_stats.collateral'
617
+ )
618
+ const crossPortfolio = toRequiredBig(
619
+ stats.cross_stats.portfolio_value,
620
+ 'stats.cross_stats.portfolio_value'
621
+ )
622
+ available = toRequiredBig(
623
+ stats.cross_stats.available_balance,
624
+ 'stats.cross_stats.available_balance'
625
+ )
626
+ const isolatedMargin = collateral.minus(crossCollateral)
627
+ const crossMargin = crossPortfolio.minus(available)
628
+ marginUsed = isolatedMargin.plus(crossMargin)
629
+ }
630
+
599
631
  this.emit(`accountSummary:${address}`, {
600
632
  channel: 'accountSummary',
601
633
  data: {
602
634
  portfolioValue: portfolio.toString(),
603
635
  availableMargin: available.toString(),
604
- marginUsed: Math.max(portfolio - available, 0).toString(),
605
- unrealizedPnl: Number.isFinite(collateral)
606
- ? (portfolio - collateral).toString()
607
- : '0',
636
+ marginUsed: marginUsed.toString(),
637
+ unrealizedPnl: portfolio.minus(collateral).toString(),
608
638
  },
609
639
  })
610
640
  }
@@ -649,7 +679,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
649
679
  } else {
650
680
  const market = this.registry?.get(String(p.market_id))
651
681
  if (market) {
652
- state.set(p.market_id, mapPosition(p, market))
682
+ state.set(p.market_id, mapPosition(p, toPerpsMarketDisplay(market)))
653
683
  }
654
684
  }
655
685
  }