@lifi/perps-sdk-provider-lighter 27.0.0 → 29.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 (76) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +38 -13
  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 +14 -7
  6. package/dist/cjs/accountSummary.js.map +1 -1
  7. package/dist/cjs/availableToTrade.d.ts +3 -0
  8. package/dist/cjs/availableToTrade.d.ts.map +1 -0
  9. package/dist/cjs/availableToTrade.js +38 -0
  10. package/dist/cjs/availableToTrade.js.map +1 -0
  11. package/dist/cjs/utils/index.d.ts +1 -1
  12. package/dist/cjs/utils/index.d.ts.map +1 -1
  13. package/dist/cjs/utils/index.js +2 -2
  14. package/dist/cjs/utils/index.js.map +1 -1
  15. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  16. package/dist/cjs/utils/mapOrder.js +2 -1
  17. package/dist/cjs/utils/mapOrder.js.map +1 -1
  18. package/dist/cjs/utils/spotPrice.d.ts +4 -0
  19. package/dist/cjs/utils/spotPrice.d.ts.map +1 -0
  20. package/dist/cjs/utils/spotPrice.js +24 -0
  21. package/dist/cjs/utils/spotPrice.js.map +1 -0
  22. package/dist/cjs/utils/transferMargin.d.ts +2 -2
  23. package/dist/cjs/utils/transferMargin.d.ts.map +1 -1
  24. package/dist/cjs/utils/transferMargin.js +33 -8
  25. package/dist/cjs/utils/transferMargin.js.map +1 -1
  26. package/dist/cjs/websocket/LighterWsProvider.js +5 -5
  27. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  28. package/dist/esm/LighterProvider.d.ts.map +1 -1
  29. package/dist/esm/LighterProvider.js +42 -17
  30. package/dist/esm/LighterProvider.js.map +1 -1
  31. package/dist/esm/accountSummary.d.ts +5 -4
  32. package/dist/esm/accountSummary.d.ts.map +1 -1
  33. package/dist/esm/accountSummary.js +19 -13
  34. package/dist/esm/accountSummary.js.map +1 -1
  35. package/dist/esm/availableToTrade.d.ts +15 -0
  36. package/dist/esm/availableToTrade.d.ts.map +1 -0
  37. package/dist/esm/availableToTrade.js +45 -0
  38. package/dist/esm/availableToTrade.js.map +1 -0
  39. package/dist/esm/utils/index.d.ts +1 -1
  40. package/dist/esm/utils/index.d.ts.map +1 -1
  41. package/dist/esm/utils/index.js +1 -1
  42. package/dist/esm/utils/index.js.map +1 -1
  43. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  44. package/dist/esm/utils/mapOrder.js +2 -1
  45. package/dist/esm/utils/mapOrder.js.map +1 -1
  46. package/dist/esm/utils/spotPrice.d.ts +14 -0
  47. package/dist/esm/utils/spotPrice.d.ts.map +1 -0
  48. package/dist/esm/utils/spotPrice.js +30 -0
  49. package/dist/esm/utils/spotPrice.js.map +1 -0
  50. package/dist/esm/utils/transferMargin.d.ts +11 -5
  51. package/dist/esm/utils/transferMargin.d.ts.map +1 -1
  52. package/dist/esm/utils/transferMargin.js +39 -11
  53. package/dist/esm/utils/transferMargin.js.map +1 -1
  54. package/dist/esm/websocket/LighterWsProvider.js +5 -5
  55. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  56. package/dist/types/LighterProvider.d.ts.map +1 -1
  57. package/dist/types/accountSummary.d.ts +5 -4
  58. package/dist/types/accountSummary.d.ts.map +1 -1
  59. package/dist/types/availableToTrade.d.ts +15 -0
  60. package/dist/types/availableToTrade.d.ts.map +1 -0
  61. package/dist/types/utils/index.d.ts +1 -1
  62. package/dist/types/utils/index.d.ts.map +1 -1
  63. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  64. package/dist/types/utils/spotPrice.d.ts +14 -0
  65. package/dist/types/utils/spotPrice.d.ts.map +1 -0
  66. package/dist/types/utils/transferMargin.d.ts +11 -5
  67. package/dist/types/utils/transferMargin.d.ts.map +1 -1
  68. package/package.json +3 -3
  69. package/src/LighterProvider.ts +70 -27
  70. package/src/accountSummary.ts +24 -16
  71. package/src/availableToTrade.ts +61 -0
  72. package/src/utils/index.ts +1 -1
  73. package/src/utils/mapOrder.ts +2 -1
  74. package/src/utils/spotPrice.ts +39 -0
  75. package/src/utils/transferMargin.ts +52 -14
  76. package/src/websocket/LighterWsProvider.ts +5 -5
@@ -5,6 +5,7 @@ import {
5
5
  explorerTxUrlFromBase,
6
6
  getAssetRegistry,
7
7
  getMarketRegistry,
8
+ getMarketsContext,
8
9
  getProviders,
9
10
  isActiveOrderStatus,
10
11
  localStorageAdapter,
@@ -14,6 +15,7 @@ import {
14
15
  type ProviderAccountExistsParams,
15
16
  type ProviderGetAccountParams,
16
17
  type ProviderGetActivityParams,
18
+ type ProviderGetAvailableToTradeParams,
17
19
  type ProviderGetDepositFlowParams,
18
20
  type ProviderGetFillsParams,
19
21
  type ProviderGetMarketSettingsParams,
@@ -40,6 +42,7 @@ import type {
40
42
  ActionStep,
41
43
  ActivitiesResponse,
42
44
  ActivityItem,
45
+ AvailableToTrade,
43
46
  Balance,
44
47
  Fill,
45
48
  FillsResponse,
@@ -63,9 +66,11 @@ import {
63
66
  MarginMode,
64
67
  PerpsErrorCode,
65
68
  } from '@lifi/perps-types'
69
+ import Big from 'big.js'
66
70
  import type { Address } from 'viem'
67
71
  import { projectLighterConfigSettings } from './accountConfig.js'
68
72
  import { getAccountSummary } from './accountSummary.js'
73
+ import { lighterAvailableToTrade } from './availableToTrade.js'
69
74
  import {
70
75
  DEFAULT_TRADES_LIMIT,
71
76
  LIGHTER_ALL_MARKETS_WILDCARD,
@@ -138,7 +143,7 @@ import {
138
143
  mapOpenPositions,
139
144
  mapOrder,
140
145
  mapPortfolioHistory,
141
- positionMarginConstraints,
146
+ positionRemovableMargin,
142
147
  toBigOrNull,
143
148
  toIsoFromMs,
144
149
  toIsoFromSeconds,
@@ -148,6 +153,7 @@ import {
148
153
  fetchRegisteredApiKey,
149
154
  normalizeLighterPublicKey,
150
155
  } from './utils/registeredApiKey.js'
156
+ import { spotPriceByAssetId } from './utils/spotPrice.js'
151
157
  import { isPlaceholderTxHash } from './utils/txHash.js'
152
158
  import { wireList } from './utils/wireList.js'
153
159
 
@@ -781,7 +787,7 @@ export const createLighterProvider = (
781
787
  }
782
788
  }
783
789
 
784
- return {
790
+ const plugin: LighterPerpsProvider = {
785
791
  type: providerKey,
786
792
 
787
793
  internalSetupActions: [ActionType.SET_REFERRAL],
@@ -825,11 +831,12 @@ export const createLighterProvider = (
825
831
 
826
832
  const registry = getMarketRegistry(requireClient(), providerKey)
827
833
  const assetRegistry = getAssetRegistry(requireClient(), providerKey)
828
- const [{ providers }, , , limitsResult, storedReadOnlyToken] =
834
+ const [{ providers }, , , { prices }, limitsResult, storedReadOnlyToken] =
829
835
  await Promise.all([
830
836
  getProviders(requireClient()),
831
837
  registry.sync(),
832
838
  assetRegistry.sync(),
839
+ getMarketsContext(requireClient(), { provider: providerKey }, opts),
833
840
  token === undefined
834
841
  ? Promise.resolve(undefined)
835
842
  : retryOnRevoked(opts, params.address, token, (resolvedToken) =>
@@ -844,12 +851,12 @@ export const createLighterProvider = (
844
851
  )
845
852
 
846
853
  const totalMarginUsed = positions.reduce(
847
- (sum, p) => sum + Number.parseFloat(p.marginUsed),
848
- 0
854
+ (sum, p) => sum.plus(toRequiredBig(p.marginUsed, 'marginUsed')),
855
+ new Big(0)
849
856
  )
850
857
  const totalUnrealizedPnl = positions.reduce(
851
- (sum, p) => sum + Number.parseFloat(p.unrealizedPnl),
852
- 0
858
+ (sum, p) => sum.plus(toRequiredBig(p.unrealizedPnl, 'unrealizedPnl')),
859
+ new Big(0)
853
860
  )
854
861
 
855
862
  const instanceMeta = providers.find((p) => p.key === providerKey)
@@ -883,23 +890,39 @@ export const createLighterProvider = (
883
890
  ]
884
891
  : []
885
892
  // Spot token holdings — non-collateral. The instance's settlement asset
886
- // is valued 1:1; other tokens have no price source at this boundary, so
887
- // their price and USD value are unknown.
888
- const balances: Balance[] = account.assets
889
- .filter((asset) => toRequiredBig(asset.balance, 'balance').gt(0))
890
- .map((a) => {
891
- const assetId = String(a.asset_id)
892
- const settlement = a.asset_id === collateral.assetIndex
893
- return {
894
- categoryId: spotCategoryId,
895
- asset:
896
- assetRegistry.get(assetId) ??
897
- lighterAsset(assetId, a.symbol, providerKey),
898
- units: a.balance,
899
- valueUsd: settlement ? a.balance : '0',
900
- ...(settlement ? { price: '1' } : {}),
901
- }
902
- })
893
+ // is valued 1:1; an asset no spot market prices keeps a zero USD value.
894
+ const heldAssets = account.assets.filter((asset) =>
895
+ toRequiredBig(asset.balance, 'balance').gt(0)
896
+ )
897
+ const spotPrices = spotPriceByAssetId(
898
+ registry.markets,
899
+ spotCategoryId,
900
+ prices,
901
+ new Set(
902
+ heldAssets
903
+ .filter((a) => a.asset_id !== collateral.assetIndex)
904
+ .map((a) => String(a.asset_id))
905
+ )
906
+ )
907
+ const balances: Balance[] = heldAssets.map((a) => {
908
+ const assetId = String(a.asset_id)
909
+ const price =
910
+ a.asset_id === collateral.assetIndex
911
+ ? new Big(1)
912
+ : spotPrices.get(assetId)
913
+ return {
914
+ categoryId: spotCategoryId,
915
+ asset:
916
+ assetRegistry.get(assetId) ??
917
+ lighterAsset(assetId, a.symbol, providerKey),
918
+ units: a.balance,
919
+ valueUsd:
920
+ price === undefined
921
+ ? '0'
922
+ : toRequiredBig(a.balance, 'balance').times(price).toFixed(),
923
+ ...(price === undefined ? {} : { price: price.toFixed() }),
924
+ }
925
+ })
903
926
 
904
927
  const assetCollateral = account.assets.flatMap((a) =>
905
928
  a.margin_mode === undefined
@@ -941,8 +964,8 @@ export const createLighterProvider = (
941
964
  balances,
942
965
  collateralBalances,
943
966
  positions,
944
- marginUsed: totalMarginUsed.toString(),
945
- unrealizedPnl: totalUnrealizedPnl.toString(),
967
+ marginUsed: totalMarginUsed.toFixed(),
968
+ unrealizedPnl: totalUnrealizedPnl.toFixed(),
946
969
  feeTier:
947
970
  limitsResult === undefined
948
971
  ? ZERO_FEE_TIER
@@ -1005,6 +1028,25 @@ export const createLighterProvider = (
1005
1028
  }
1006
1029
  },
1007
1030
 
1031
+ /**
1032
+ * Lighter publishes no per-market figure, so this derives one from the
1033
+ * account's `available_balance` and its open position on the market.
1034
+ */
1035
+ async getAvailableToTrade(
1036
+ params: ProviderGetAvailableToTradeParams,
1037
+ opts?: SDKRequestOptions
1038
+ ): Promise<AvailableToTrade | undefined> {
1039
+ const registry = getMarketRegistry(requireClient(), providerKey)
1040
+ await registry.sync()
1041
+ const market = registry.require(params.marketId)
1042
+ if (market.categoryId === LIGHTER_SPOT_CATEGORY_ID) {
1043
+ return undefined
1044
+ }
1045
+ const account = await plugin.getAccount({ address: params.address }, opts)
1046
+ const { availableMargin } = getAccountSummary(account, account.positions)
1047
+ return lighterAvailableToTrade(market, availableMargin, account.positions)
1048
+ },
1049
+
1008
1050
  /**
1009
1051
  * Lighter reports a market's margin mode and leverage only on the
1010
1052
  * account's position row, so a market the account never touched (or a
@@ -1603,7 +1645,7 @@ export const createLighterProvider = (
1603
1645
 
1604
1646
  estimateLiquidationPrice,
1605
1647
 
1606
- positionMarginConstraints,
1648
+ positionRemovableMargin,
1607
1649
 
1608
1650
  projectConfig(
1609
1651
  config: AccountConfig,
@@ -1675,6 +1717,7 @@ export const createLighterProvider = (
1675
1717
  )
1676
1718
  },
1677
1719
  }
1720
+ return plugin
1678
1721
  }
1679
1722
 
1680
1723
  /**
@@ -7,23 +7,29 @@ import type {
7
7
  } from '@lifi/perps-types'
8
8
  import { PerpsErrorCode } from '@lifi/perps-types'
9
9
  import Big from 'big.js'
10
+ import { LIGHTER_PROVIDER_KEY, LIGHTER_RH_PROVIDER_KEY } from './constants.js'
10
11
  import { toRequiredBig } from './utils/decimal.js'
11
12
 
12
13
  const lighterConfig = (account: AccountResponse): LighterAccountConfig => {
13
- if (account.config.provider !== 'lighter') {
14
+ const { config } = account
15
+ if (
16
+ config.provider !== LIGHTER_PROVIDER_KEY &&
17
+ config.provider !== LIGHTER_RH_PROVIDER_KEY
18
+ ) {
14
19
  throw new PerpsError(
15
20
  PerpsErrorCode.SDKError,
16
- `Lighter account summary received a '${account.config.provider}' account config`
21
+ `Lighter account summary received a '${config.provider}' account config`
17
22
  )
18
23
  }
19
- return account.config
24
+ return config
20
25
  }
21
26
 
22
27
  /**
23
- * Roll a Lighter account up into an {@link AccountSummary}, from the venue
24
- * figures the response carries. `totalAssetValue` is total equity and
25
- * `availableBalance` is buying power, so neither needs reconciling against the
26
- * positions; the positions supply only the margin and PnL breakdown.
28
+ * Roll a Lighter account up into an {@link AccountSummary}. `totalAssetValue`
29
+ * is perps-route equity and excludes the spot route, so `portfolioValue` adds
30
+ * every spot `balances` row, settlement included; the `collateralBalances` row
31
+ * is buying power already inside `totalAssetValue`. The positions supply only
32
+ * the margin and PnL breakdown.
27
33
  *
28
34
  * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
29
35
  * @public
@@ -41,18 +47,20 @@ export function getAccountSummary(
41
47
  unrealizedPnl = unrealizedPnl.plus(position.unrealizedPnl)
42
48
  }
43
49
 
50
+ let portfolioValue = toRequiredBig(config.totalAssetValue, 'totalAssetValue')
51
+ for (const balance of account.balances) {
52
+ portfolioValue = portfolioValue.plus(
53
+ toRequiredBig(balance.valueUsd, 'valueUsd')
54
+ )
55
+ }
56
+
44
57
  return {
45
- // `total_asset_value` already covers every asset the venue prices, so the
46
- // spot `balances` rows must not be added on top of it.
47
- portfolioValue: toRequiredBig(
48
- config.totalAssetValue,
49
- 'totalAssetValue'
50
- ).toString(),
58
+ portfolioValue: portfolioValue.toFixed(),
51
59
  availableMargin: toRequiredBig(
52
60
  config.availableBalance,
53
61
  'availableBalance'
54
- ).toString(),
55
- marginUsed: marginUsed.toString(),
56
- unrealizedPnl: unrealizedPnl.toString(),
62
+ ).toFixed(),
63
+ marginUsed: marginUsed.toFixed(),
64
+ unrealizedPnl: unrealizedPnl.toFixed(),
57
65
  }
58
66
  }
@@ -0,0 +1,61 @@
1
+ import { toAssetDisplay } from '@lifi/perps-sdk'
2
+ import type { AvailableToTrade, Market, Position } from '@lifi/perps-types'
3
+ import { MarginMode, PositionSide } from '@lifi/perps-types'
4
+ import Big from 'big.js'
5
+ import { toRequiredBig } from './utils/decimal.js'
6
+
7
+ const atLeastZero = (value: Big): Big => (value.lt(0) ? new Big(0) : value)
8
+
9
+ // Lighter's `available_balance` already nets cross unrealized PnL and counts
10
+ // an isolated position's equity above its IMR, so only the rest is released.
11
+ const releasedOnClose = (position: Position): Big => {
12
+ const marginUsed = toRequiredBig(position.marginUsed, 'marginUsed')
13
+ if (position.marginMode !== MarginMode.ISOLATED) {
14
+ return marginUsed
15
+ }
16
+ const equity = marginUsed.plus(
17
+ toRequiredBig(position.unrealizedPnl, 'unrealizedPnl')
18
+ )
19
+ const imr = toRequiredBig(
20
+ position.initialMarginRequirement,
21
+ 'initialMarginRequirement'
22
+ )
23
+ return equity.lt(imr) ? equity : imr
24
+ }
25
+
26
+ /**
27
+ * The amounts the account can still buy and sell on one Lighter perps
28
+ * market. The side that adds to an open position on `market` gets
29
+ * `availableMargin`. The side that reduces or flips it gets the position's
30
+ * initial margin requirement, which the closing part of the order consumes,
31
+ * plus `availableMargin` and the margin that closing releases, floored at 0.
32
+ * A deficit therefore never cuts the closing part. Both amounts are at least 0.
33
+ *
34
+ * @param availableMargin - The account's {@link AccountSummary.availableMargin}.
35
+ * @throws {PerpsError} `SDKError` when a decimal the formula reads is malformed.
36
+ * @internal
37
+ */
38
+ export const lighterAvailableToTrade = (
39
+ market: Market,
40
+ availableMargin: string,
41
+ positions: readonly Position[]
42
+ ): AvailableToTrade => {
43
+ const available = toRequiredBig(availableMargin, 'availableMargin')
44
+ const adding = atLeastZero(available)
45
+ const position = positions.find((p) => p.market.id === market.id)
46
+ const reducing =
47
+ position === undefined
48
+ ? adding
49
+ : toRequiredBig(
50
+ position.initialMarginRequirement,
51
+ 'initialMarginRequirement'
52
+ ).plus(atLeastZero(available.plus(releasedOnClose(position))))
53
+ const isShort = position?.side === PositionSide.SHORT
54
+ return {
55
+ providerId: market.providerId,
56
+ marketId: market.id,
57
+ asset: toAssetDisplay(market.quoteAsset),
58
+ buy: (isShort ? reducing : adding).toFixed(),
59
+ sell: (isShort ? adding : reducing).toFixed(),
60
+ }
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
  }
@@ -660,10 +660,10 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
660
660
  this.emit(`accountSummary:${address}`, {
661
661
  channel: 'accountSummary',
662
662
  data: {
663
- portfolioValue: portfolio.toString(),
664
- availableMargin: available.toString(),
665
- marginUsed: marginUsed.toString(),
666
- unrealizedPnl: portfolio.minus(collateral).toString(),
663
+ portfolioValue: portfolio.toFixed(),
664
+ availableMargin: available.toFixed(),
665
+ marginUsed: marginUsed.toFixed(),
666
+ unrealizedPnl: portfolio.minus(collateral).toFixed(),
667
667
  },
668
668
  })
669
669
  }
@@ -1008,5 +1008,5 @@ export const lighterWsProvider = (
1008
1008
  Object.assign(
1009
1009
  ({ provider, wsUrl, client }: WsProviderFactoryParams) =>
1010
1010
  new LighterWsProvider(wsUrl, provider, options, client),
1011
- { streamsCandles: false }
1011
+ { streamsCandles: false, streamsAvailableToTrade: false }
1012
1012
  )