@lifi/perps-sdk-provider-lighter 6.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 (116) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +77 -37
  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/depositFlow.d.ts +4 -0
  8. package/dist/cjs/depositFlow.d.ts.map +1 -0
  9. package/dist/cjs/depositFlow.js +32 -0
  10. package/dist/cjs/depositFlow.js.map +1 -0
  11. package/dist/cjs/index.d.ts +1 -0
  12. package/dist/cjs/index.d.ts.map +1 -1
  13. package/dist/cjs/index.js +3 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/types/account.d.ts +1 -0
  16. package/dist/cjs/types/account.d.ts.map +1 -1
  17. package/dist/cjs/types/ws.d.ts +5 -0
  18. package/dist/cjs/types/ws.d.ts.map +1 -1
  19. package/dist/cjs/utils/decimal.d.ts +5 -0
  20. package/dist/cjs/utils/decimal.d.ts.map +1 -0
  21. package/dist/cjs/utils/decimal.js +38 -0
  22. package/dist/cjs/utils/decimal.js.map +1 -0
  23. package/dist/cjs/utils/index.d.ts +3 -1
  24. package/dist/cjs/utils/index.d.ts.map +1 -1
  25. package/dist/cjs/utils/index.js +7 -1
  26. package/dist/cjs/utils/index.js.map +1 -1
  27. package/dist/cjs/utils/mapOpenPositions.d.ts +2 -2
  28. package/dist/cjs/utils/mapOpenPositions.d.ts.map +1 -1
  29. package/dist/cjs/utils/mapOpenPositions.js.map +1 -1
  30. package/dist/cjs/utils/mapPosition.d.ts +3 -2
  31. package/dist/cjs/utils/mapPosition.d.ts.map +1 -1
  32. package/dist/cjs/utils/mapPosition.js +30 -9
  33. package/dist/cjs/utils/mapPosition.js.map +1 -1
  34. package/dist/cjs/utils/transferMargin.d.ts +3 -0
  35. package/dist/cjs/utils/transferMargin.d.ts.map +1 -0
  36. package/dist/cjs/utils/transferMargin.js +17 -0
  37. package/dist/cjs/utils/transferMargin.js.map +1 -0
  38. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  39. package/dist/cjs/websocket/LighterWsProvider.js +20 -9
  40. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  41. package/dist/esm/LighterProvider.d.ts.map +1 -1
  42. package/dist/esm/LighterProvider.js +67 -22
  43. package/dist/esm/LighterProvider.js.map +1 -1
  44. package/dist/esm/accountSummary.d.ts +5 -4
  45. package/dist/esm/accountSummary.d.ts.map +1 -1
  46. package/dist/esm/accountSummary.js +35 -6
  47. package/dist/esm/accountSummary.js.map +1 -1
  48. package/dist/esm/depositFlow.d.ts +12 -0
  49. package/dist/esm/depositFlow.d.ts.map +1 -0
  50. package/dist/esm/depositFlow.js +37 -0
  51. package/dist/esm/depositFlow.js.map +1 -0
  52. package/dist/esm/index.d.ts +1 -0
  53. package/dist/esm/index.d.ts.map +1 -1
  54. package/dist/esm/index.js +1 -0
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/types/account.d.ts +3 -0
  57. package/dist/esm/types/account.d.ts.map +1 -1
  58. package/dist/esm/types/ws.d.ts +9 -1
  59. package/dist/esm/types/ws.d.ts.map +1 -1
  60. package/dist/esm/utils/decimal.d.ts +15 -0
  61. package/dist/esm/utils/decimal.d.ts.map +1 -0
  62. package/dist/esm/utils/decimal.js +39 -0
  63. package/dist/esm/utils/decimal.js.map +1 -0
  64. package/dist/esm/utils/index.d.ts +3 -1
  65. package/dist/esm/utils/index.d.ts.map +1 -1
  66. package/dist/esm/utils/index.js +3 -1
  67. package/dist/esm/utils/index.js.map +1 -1
  68. package/dist/esm/utils/mapOpenPositions.d.ts +2 -2
  69. package/dist/esm/utils/mapOpenPositions.d.ts.map +1 -1
  70. package/dist/esm/utils/mapOpenPositions.js.map +1 -1
  71. package/dist/esm/utils/mapPosition.d.ts +10 -2
  72. package/dist/esm/utils/mapPosition.d.ts.map +1 -1
  73. package/dist/esm/utils/mapPosition.js +32 -11
  74. package/dist/esm/utils/mapPosition.js.map +1 -1
  75. package/dist/esm/utils/transferMargin.d.ts +11 -0
  76. package/dist/esm/utils/transferMargin.d.ts.map +1 -0
  77. package/dist/esm/utils/transferMargin.js +22 -0
  78. package/dist/esm/utils/transferMargin.js.map +1 -0
  79. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  80. package/dist/esm/websocket/LighterWsProvider.js +24 -13
  81. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  82. package/dist/types/LighterProvider.d.ts.map +1 -1
  83. package/dist/types/accountSummary.d.ts +5 -4
  84. package/dist/types/accountSummary.d.ts.map +1 -1
  85. package/dist/types/depositFlow.d.ts +12 -0
  86. package/dist/types/depositFlow.d.ts.map +1 -0
  87. package/dist/types/index.d.ts +1 -0
  88. package/dist/types/index.d.ts.map +1 -1
  89. package/dist/types/types/account.d.ts +3 -0
  90. package/dist/types/types/account.d.ts.map +1 -1
  91. package/dist/types/types/ws.d.ts +9 -1
  92. package/dist/types/types/ws.d.ts.map +1 -1
  93. package/dist/types/utils/decimal.d.ts +15 -0
  94. package/dist/types/utils/decimal.d.ts.map +1 -0
  95. package/dist/types/utils/index.d.ts +3 -1
  96. package/dist/types/utils/index.d.ts.map +1 -1
  97. package/dist/types/utils/mapOpenPositions.d.ts +2 -2
  98. package/dist/types/utils/mapOpenPositions.d.ts.map +1 -1
  99. package/dist/types/utils/mapPosition.d.ts +10 -2
  100. package/dist/types/utils/mapPosition.d.ts.map +1 -1
  101. package/dist/types/utils/transferMargin.d.ts +11 -0
  102. package/dist/types/utils/transferMargin.d.ts.map +1 -0
  103. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  104. package/package.json +3 -3
  105. package/src/LighterProvider.ts +107 -21
  106. package/src/accountSummary.ts +38 -6
  107. package/src/depositFlow.ts +59 -0
  108. package/src/index.ts +1 -0
  109. package/src/types/account.ts +3 -0
  110. package/src/types/ws.ts +9 -1
  111. package/src/utils/decimal.ts +52 -0
  112. package/src/utils/index.ts +3 -1
  113. package/src/utils/mapOpenPositions.ts +2 -2
  114. package/src/utils/mapPosition.ts +40 -14
  115. package/src/utils/transferMargin.ts +35 -0
  116. package/src/websocket/LighterWsProvider.ts +41 -11
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type DepositFlow,
2
3
  explorerTxUrlFromBase,
3
4
  getAssetRegistry,
4
5
  getMarketRegistry,
@@ -9,7 +10,9 @@ import {
9
10
  type ProviderAccountExistsParams,
10
11
  type ProviderGetAccountParams,
11
12
  type ProviderGetActivityParams,
13
+ type ProviderGetDepositFlowParams,
12
14
  type ProviderGetFillsParams,
15
+ type ProviderGetMarketSettingsParams,
13
16
  type ProviderGetOrderParams,
14
17
  type ProviderGetOrdersParams,
15
18
  type ProviderGetPositionsParams,
@@ -18,6 +21,7 @@ import {
18
21
  resolveRetryPolicy,
19
22
  type SDKRequestOptions,
20
23
  type SignActionsContext,
24
+ toPerpsMarketDisplay,
21
25
  } from '@lifi/perps-sdk'
22
26
  import type {
23
27
  AccountConfig,
@@ -31,6 +35,7 @@ import type {
31
35
  FillsResponse,
32
36
  LighterAccountConfig,
33
37
  LighterProviderKey,
38
+ MarketSettings,
34
39
  Order,
35
40
  OrdersResponse,
36
41
  Position,
@@ -40,7 +45,12 @@ import type {
40
45
  SignedActionStep,
41
46
  SigningMethod,
42
47
  } from '@lifi/perps-types'
43
- import { ActionType, ActivityType, PerpsErrorCode } from '@lifi/perps-types'
48
+ import {
49
+ ActionType,
50
+ ActivityType,
51
+ MarginMode,
52
+ PerpsErrorCode,
53
+ } from '@lifi/perps-types'
44
54
  import type { Address } from 'viem'
45
55
  import { projectLighterConfigSettings } from './accountConfig.js'
46
56
  import { getAccountSummary } from './accountSummary.js'
@@ -56,6 +66,7 @@ import {
56
66
  LIGHTER_PROVIDER_KEY,
57
67
  LIGHTER_SPOT_CATEGORY_ID,
58
68
  } from './constants.js'
69
+ import { lighterDepositFlow } from './depositFlow.js'
59
70
  import { createAuthToken } from './signers/createAuthToken.js'
60
71
  import type { LighterKeyStore } from './signers/LighterKeyStore.js'
61
72
  import type { LighterReadOnlyTokenManagerOptions } from './signers/LighterReadOnlyTokenManager.js'
@@ -74,6 +85,7 @@ import type {
74
85
  LtTransferHistoryResponse,
75
86
  LtWithdrawHistoryResponse,
76
87
  } from './types/index.js'
88
+ import { LT_MARGIN_MODE_ISOLATED } from './types/index.js'
77
89
  import {
78
90
  decodeActivityCursor,
79
91
  encodeActivityCursor,
@@ -91,12 +103,15 @@ import {
91
103
  fetchDetailedAccount,
92
104
  formatOrderPrice,
93
105
  formatOrderSize,
106
+ leverageFromImf,
94
107
  lighterAsset,
95
108
  mapFill,
96
109
  mapOpenPositions,
97
110
  mapOrderDetail,
111
+ positionMarginConstraints,
98
112
  toIsoFromMs,
99
113
  toIsoFromSeconds,
114
+ toRequiredBig,
100
115
  } from './utils/index.js'
101
116
 
102
117
  const ZERO_FEE_TIER = { maker: '0', taker: '0' }
@@ -666,6 +681,24 @@ export const lighterProvider = (
666
681
  return { deposits, withdraws, fundings, liquidations, transfers }
667
682
  }
668
683
 
684
+ const resolveAccountExists = async (
685
+ address: Address,
686
+ opts?: SDKRequestOptions
687
+ ): Promise<boolean> => {
688
+ try {
689
+ await fetchDetailedAccount(apiClient(opts), address)
690
+ return true
691
+ } catch (err) {
692
+ if (
693
+ err instanceof PerpsError &&
694
+ err.code === PerpsErrorCode.AccountNotFound
695
+ ) {
696
+ return false
697
+ }
698
+ throw err
699
+ }
700
+ }
701
+
669
702
  return {
670
703
  type: providerKey,
671
704
 
@@ -734,7 +767,7 @@ export const lighterProvider = (
734
767
  normalizeLighterPublicKey(registeredKey.public_key)
735
768
 
736
769
  const positions: Position[] = mapOpenPositions(account.positions, (id) =>
737
- registry.require(String(id))
770
+ toPerpsMarketDisplay(registry.require(String(id)))
738
771
  )
739
772
 
740
773
  const totalMarginUsed = positions.reduce(
@@ -753,16 +786,24 @@ export const lighterProvider = (
753
786
  categories.find((c) => c.quoteAsset === null)?.id ??
754
787
  LIGHTER_SPOT_CATEGORY_ID
755
788
 
756
- // USDC collateral is the category quote asset → collateralBalances.
757
- // `available_balance` is the free collateral (Lighter's `collateral` is
758
- // gross, i.e. includes margin locked in positions); the locked portion is
759
- // carried by the positions' `marginUsed`.
789
+ // Cross buying power is isolated from per-position allocations. Lighter
790
+ // reports cross equity (already marked by cross uPnL) separately from
791
+ // the initial margin locked by cross positions.
792
+ const availableMargin = toRequiredBig(
793
+ account.cross_asset_value,
794
+ 'cross_asset_value'
795
+ ).minus(
796
+ toRequiredBig(
797
+ account.cross_initial_margin_requirement,
798
+ 'cross_initial_margin_requirement'
799
+ )
800
+ )
760
801
  const collateralBalances: Balance[] = [
761
802
  {
762
803
  categoryId: perpsCategory?.id ?? providerKey,
763
804
  asset: perpsCategory?.quoteAsset ?? lighterAsset('USDC', 'USDC'),
764
- units: account.available_balance,
765
- valueUsd: account.available_balance,
805
+ units: availableMargin.toString(),
806
+ valueUsd: availableMargin.toString(),
766
807
  },
767
808
  ]
768
809
  // Spot token holdings — non-collateral. USDC value is 1:1; other tokens
@@ -826,18 +867,17 @@ export const lighterProvider = (
826
867
  params: ProviderAccountExistsParams,
827
868
  opts?: SDKRequestOptions
828
869
  ): Promise<boolean> {
829
- try {
830
- await fetchDetailedAccount(apiClient(opts), params.address)
831
- return true
832
- } catch (err) {
833
- if (
834
- err instanceof PerpsError &&
835
- err.code === PerpsErrorCode.AccountNotFound
836
- ) {
837
- return false
838
- }
839
- throw err
840
- }
870
+ return resolveAccountExists(params.address, opts)
871
+ },
872
+
873
+ async getDepositFlow(
874
+ params: ProviderGetDepositFlowParams,
875
+ opts?: SDKRequestOptions
876
+ ): Promise<DepositFlow> {
877
+ return lighterDepositFlow(
878
+ providerKey,
879
+ await resolveAccountExists(params.address, opts)
880
+ )
841
881
  },
842
882
 
843
883
  async getPositions(
@@ -852,7 +892,7 @@ export const lighterProvider = (
852
892
  ])
853
893
 
854
894
  let positions: Position[] = mapOpenPositions(account.positions, (id) =>
855
- registry.require(String(id))
895
+ toPerpsMarketDisplay(registry.require(String(id)))
856
896
  )
857
897
 
858
898
  if (params.marketId !== undefined) {
@@ -866,6 +906,50 @@ export const lighterProvider = (
866
906
  }
867
907
  },
868
908
 
909
+ /**
910
+ * Lighter reports a market's margin mode and leverage only on the
911
+ * account's position row, so a market the account never touched (or a
912
+ * missing account) resolves `undefined` rather than a venue default.
913
+ */
914
+ async getMarketSettings(
915
+ params: ProviderGetMarketSettingsParams,
916
+ opts?: SDKRequestOptions
917
+ ): Promise<MarketSettings | undefined> {
918
+ // Spot markets carry no margin mode or leverage.
919
+ if (params.market.categoryId === LIGHTER_SPOT_CATEGORY_ID) {
920
+ return undefined
921
+ }
922
+ let account: LtDetailedAccount
923
+ try {
924
+ account = await fetchDetailedAccount(apiClient(opts), params.address)
925
+ } catch (err) {
926
+ if (
927
+ err instanceof PerpsError &&
928
+ err.code === PerpsErrorCode.AccountNotFound
929
+ ) {
930
+ return undefined
931
+ }
932
+ throw err
933
+ }
934
+ const row = account.positions.find(
935
+ (p) => String(p.market_id) === params.market.marketId
936
+ )
937
+ if (!row) {
938
+ return undefined
939
+ }
940
+ const leverage = leverageFromImf(row.initial_margin_fraction)
941
+ if (leverage === undefined) {
942
+ return undefined
943
+ }
944
+ return {
945
+ marginMode:
946
+ row.margin_mode === LT_MARGIN_MODE_ISOLATED
947
+ ? MarginMode.ISOLATED
948
+ : MarginMode.CROSS,
949
+ leverage,
950
+ }
951
+ },
952
+
869
953
  /**
870
954
  * Lighter's `accountActiveOrders` endpoint takes no limit/cursor and
871
955
  * returns every active order for the account, so the response is always
@@ -1235,6 +1319,8 @@ export const lighterProvider = (
1235
1319
 
1236
1320
  estimateLiquidationPrice,
1237
1321
 
1322
+ positionMarginConstraints,
1323
+
1238
1324
  projectConfig(
1239
1325
  config: AccountConfig,
1240
1326
  setup: ProviderAction[],
@@ -1,16 +1,18 @@
1
- import { summarizeAccount } from '@lifi/perps-sdk'
2
1
  import type {
3
2
  AccountResponse,
4
3
  AccountSummary,
5
4
  Position,
6
5
  } from '@lifi/perps-types'
6
+ import { MarginMode } from '@lifi/perps-types'
7
+ import Big from 'big.js'
7
8
 
8
9
  /**
9
10
  * Roll a Lighter {@link AccountResponse} up into an {@link AccountSummary}.
10
- * Lighter has a single flat collateral model with no abstraction modes: the
11
- * collateral rows hold `available_balance`, which nets locked margin out but
12
- * marks unrealized PnL in — so only the positions' `marginUsed` is added
13
- * back for the portfolio value, never their PnL.
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.
14
16
  *
15
17
  * @public
16
18
  */
@@ -18,5 +20,35 @@ export function getAccountSummary(
18
20
  account: AccountResponse,
19
21
  positions: Position[]
20
22
  ): AccountSummary {
21
- return summarizeAccount(account, positions, 'net')
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
+ }
32
+
33
+ let marginUsed = new Big(0)
34
+ let unrealizedPnl = new Big(0)
35
+ let isolatedUnrealizedPnl = new Big(0)
36
+ for (const position of positions) {
37
+ marginUsed = marginUsed.plus(position.marginUsed)
38
+ unrealizedPnl = unrealizedPnl.plus(position.unrealizedPnl)
39
+ if (position.marginMode === MarginMode.ISOLATED) {
40
+ isolatedUnrealizedPnl = isolatedUnrealizedPnl.plus(position.unrealizedPnl)
41
+ }
42
+ }
43
+
44
+ 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
+ }
22
54
  }
@@ -0,0 +1,59 @@
1
+ import {
2
+ type DeclaredDepositAsset,
3
+ type DepositFlow,
4
+ ETHEREUM_NATIVE_GAS,
5
+ ETHEREUM_USDC,
6
+ LIGHTER_USDC,
7
+ ROBINHOOD_NATIVE_GAS,
8
+ ROBINHOOD_USDG,
9
+ } from '@lifi/perps-sdk'
10
+ import { ActionType, type LighterProviderKey } from '@lifi/perps-types'
11
+ import { LIGHTER_PROVIDER_KEY, LIGHTER_RH_PROVIDER_KEY } from './constants.js'
12
+
13
+ interface LighterDepositAssets {
14
+ /** Collateral token on the venue chain, credited to an account that exists. */
15
+ venueCollateral: DeclaredDepositAsset
16
+ /** Collateral the account-opening deposit is made in, on the chain that deposit runs on. */
17
+ firstDepositCollateral: DeclaredDepositAsset
18
+ firstDepositGas: DeclaredDepositAsset
19
+ }
20
+
21
+ const DEPOSIT_ASSETS: Record<LighterProviderKey, LighterDepositAssets> = {
22
+ [LIGHTER_PROVIDER_KEY]: {
23
+ venueCollateral: LIGHTER_USDC,
24
+ firstDepositCollateral: ETHEREUM_USDC,
25
+ firstDepositGas: ETHEREUM_NATIVE_GAS,
26
+ },
27
+ [LIGHTER_RH_PROVIDER_KEY]: {
28
+ venueCollateral: ROBINHOOD_USDG,
29
+ firstDepositCollateral: ROBINHOOD_USDG,
30
+ firstDepositGas: ROBINHOOD_NATIVE_GAS,
31
+ },
32
+ }
33
+
34
+ /**
35
+ * Resolve one Lighter instance's deposit flow from whether the account already
36
+ * exists: an existing account takes a swap into the instance's venue collateral,
37
+ * while an address with no account runs the account-opening deposit on the chain
38
+ * that instance's collateral is deposited from.
39
+ *
40
+ * @public
41
+ */
42
+ export function lighterDepositFlow(
43
+ providerKey: LighterProviderKey,
44
+ accountExists: boolean
45
+ ): DepositFlow {
46
+ const assets = DEPOSIT_ASSETS[providerKey]
47
+
48
+ if (accountExists) {
49
+ return { kind: 'lifiSwap', destination: assets.venueCollateral }
50
+ }
51
+
52
+ return {
53
+ kind: 'firstDepositPipeline',
54
+ chainId: assets.firstDepositCollateral.chainId,
55
+ gasAsset: assets.firstDepositGas,
56
+ collateral: assets.firstDepositCollateral,
57
+ bridgeAction: ActionType.DEPOSIT,
58
+ }
59
+ }
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export {
33
33
  LIGHTER_SPOT_CATEGORY_ID,
34
34
  lighterRhInstance,
35
35
  } from './constants.js'
36
+ export { lighterDepositFlow } from './depositFlow.js'
36
37
  export type {
37
38
  LighterPerpsProvider,
38
39
  LighterProviderOptions,
@@ -79,6 +79,9 @@ export interface LtDetailedAccount {
79
79
  assets: LtAccountAsset[]
80
80
  total_asset_value: string
81
81
  cross_asset_value: string
82
+ /** Initial margin locked by cross positions; isolated positions carry
83
+ * their own `allocated_margin` instead. */
84
+ cross_initial_margin_requirement: string
82
85
  approved_integrators?: LtApprovedIntegrator[]
83
86
  }
84
87
 
package/src/types/ws.ts CHANGED
@@ -150,7 +150,10 @@ export type LtWsAccountAllTradesMessage = LtWsMessage & {
150
150
  }
151
151
 
152
152
  /**
153
- * `user_stats/{account_index}` frame; decimal strings in USD.
153
+ * `user_stats/{account_index}` frame; decimal strings in USD. The top-level
154
+ * figures span the whole account (`available_balance` = total withdrawable,
155
+ * including isolated positions' excess margin); `cross_stats` scopes them to
156
+ * the cross pool, whose `available_balance` is the free tradable collateral.
154
157
  * @public
155
158
  */
156
159
  export type LtWsUserStatsMessage = LtWsMessage & {
@@ -158,6 +161,11 @@ export type LtWsUserStatsMessage = LtWsMessage & {
158
161
  collateral?: string
159
162
  portfolio_value?: string
160
163
  available_balance?: string
164
+ cross_stats?: {
165
+ collateral: string
166
+ portfolio_value: string
167
+ available_balance: string
168
+ }
161
169
  }
162
170
  }
163
171
 
@@ -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
+ }