@lifi/perps-sdk-provider-lighter 28.0.0 → 29.1.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 (35) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +46 -28
  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 +6 -3
  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/websocket/LighterWsProvider.js +5 -5
  12. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  13. package/dist/esm/LighterProvider.d.ts.map +1 -1
  14. package/dist/esm/LighterProvider.js +57 -32
  15. package/dist/esm/LighterProvider.js.map +1 -1
  16. package/dist/esm/accountSummary.d.ts +3 -3
  17. package/dist/esm/accountSummary.d.ts.map +1 -1
  18. package/dist/esm/accountSummary.js +9 -6
  19. package/dist/esm/accountSummary.js.map +1 -1
  20. package/dist/esm/availableToTrade.d.ts +15 -0
  21. package/dist/esm/availableToTrade.d.ts.map +1 -0
  22. package/dist/esm/availableToTrade.js +45 -0
  23. package/dist/esm/availableToTrade.js.map +1 -0
  24. package/dist/esm/websocket/LighterWsProvider.js +5 -5
  25. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  26. package/dist/types/LighterProvider.d.ts.map +1 -1
  27. package/dist/types/accountSummary.d.ts +3 -3
  28. package/dist/types/accountSummary.d.ts.map +1 -1
  29. package/dist/types/availableToTrade.d.ts +15 -0
  30. package/dist/types/availableToTrade.d.ts.map +1 -0
  31. package/package.json +3 -3
  32. package/src/LighterProvider.ts +88 -40
  33. package/src/accountSummary.ts +11 -6
  34. package/src/availableToTrade.ts +61 -0
  35. package/src/websocket/LighterWsProvider.ts +5 -5
@@ -2,9 +2,9 @@ import type { AccountResponse, AccountSummary, Position } from '@lifi/perps-type
2
2
  /**
3
3
  * Roll a Lighter account up into an {@link AccountSummary}. `totalAssetValue`
4
4
  * is perps-route equity and excludes the spot route, so `portfolioValue` adds
5
- * every spot `balances` row, settlement included; the `collateralBalances` row
6
- * is buying power already inside `totalAssetValue`. The positions supply only
7
- * the margin and PnL breakdown.
5
+ * every spot `balances` row, settlement included; the `collateralBalances` rows
6
+ * are the perps-route holding already inside `totalAssetValue`. The positions
7
+ * supply only the margin and PnL breakdown.
8
8
  *
9
9
  * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
10
10
  * @public
@@ -1 +1 @@
1
- {"version":3,"file":"accountSummary.d.ts","sourceRoot":"","sources":["../../src/accountSummary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EAEd,QAAQ,EACT,MAAM,mBAAmB,CAAA;AAe1B;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,QAAQ,EAAE,GACpB,cAAc,CA0BhB"}
1
+ {"version":3,"file":"accountSummary.d.ts","sourceRoot":"","sources":["../../src/accountSummary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EAEd,QAAQ,EACT,MAAM,mBAAmB,CAAA;AAoB1B;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,QAAQ,EAAE,GACpB,cAAc,CA0BhB"}
@@ -0,0 +1,15 @@
1
+ import type { AvailableToTrade, Market, Position } from '@lifi/perps-types';
2
+ /**
3
+ * The amounts the account can still buy and sell on one Lighter perps
4
+ * market. The side that adds to an open position on `market` gets
5
+ * `availableMargin`. The side that reduces or flips it gets the position's
6
+ * initial margin requirement, which the closing part of the order consumes,
7
+ * plus `availableMargin` and the margin that closing releases, floored at 0.
8
+ * A deficit therefore never cuts the closing part. Both amounts are at least 0.
9
+ *
10
+ * @param availableMargin - The account's {@link AccountSummary.availableMargin}.
11
+ * @throws {PerpsError} `SDKError` when a decimal the formula reads is malformed.
12
+ * @internal
13
+ */
14
+ export declare const lighterAvailableToTrade: (market: Market, availableMargin: string, positions: readonly Position[]) => AvailableToTrade;
15
+ //# sourceMappingURL=availableToTrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"availableToTrade.d.ts","sourceRoot":"","sources":["../../src/availableToTrade.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAwB3E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,EACd,iBAAiB,MAAM,EACvB,WAAW,SAAS,QAAQ,EAAE,KAC7B,gBAmBF,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/perps-sdk-provider-lighter",
3
- "version": "28.0.0",
3
+ "version": "29.1.0",
4
4
  "description": "Lighter provider plugin for @lifi/perps-sdk",
5
5
  "homepage": "https://github.com/lifinance/perps-sdk",
6
6
  "bugs": {
@@ -44,10 +44,10 @@
44
44
  "dependencies": {
45
45
  "big.js": "^7.0.1",
46
46
  "viem": "^2.55.18",
47
- "@lifi/perps-types": "^16.0.0"
47
+ "@lifi/perps-types": "^16.1.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@lifi/perps-sdk": "^17.0.0"
50
+ "@lifi/perps-sdk": "^18.0.0"
51
51
  },
52
52
  "publishConfig": {
53
53
  "access": "public"
@@ -15,6 +15,7 @@ import {
15
15
  type ProviderAccountExistsParams,
16
16
  type ProviderGetAccountParams,
17
17
  type ProviderGetActivityParams,
18
+ type ProviderGetAvailableToTradeParams,
18
19
  type ProviderGetDepositFlowParams,
19
20
  type ProviderGetFillsParams,
20
21
  type ProviderGetMarketSettingsParams,
@@ -41,6 +42,8 @@ import type {
41
42
  ActionStep,
42
43
  ActivitiesResponse,
43
44
  ActivityItem,
45
+ Asset,
46
+ AvailableToTrade,
44
47
  Balance,
45
48
  Fill,
46
49
  FillsResponse,
@@ -68,6 +71,7 @@ import Big from 'big.js'
68
71
  import type { Address } from 'viem'
69
72
  import { projectLighterConfigSettings } from './accountConfig.js'
70
73
  import { getAccountSummary } from './accountSummary.js'
74
+ import { lighterAvailableToTrade } from './availableToTrade.js'
71
75
  import {
72
76
  DEFAULT_TRADES_LIMIT,
73
77
  LIGHTER_ALL_MARKETS_WILDCARD,
@@ -94,6 +98,7 @@ import {
94
98
  } from './signers/signActions.js'
95
99
  import type {
96
100
  LtAccount,
101
+ LtAccountAsset,
97
102
  LtAccountLimits,
98
103
  LtAccountOrdersResponse,
99
104
  LtAccountPnL,
@@ -159,6 +164,13 @@ const ZERO_FEE_TIER = { maker: '0', taker: '0' }
159
164
  const tickToFeeString = (tick: number): string =>
160
165
  String(tick / LIGHTER_FEE_TICK_SCALE)
161
166
 
167
+ const transferableWithin = (venueFigure: Big, units: Big): string => {
168
+ if (venueFigure.lt(0)) {
169
+ return '0'
170
+ }
171
+ return (venueFigure.gt(units) ? units : venueFigure).toFixed()
172
+ }
173
+
162
174
  const projectFeeTier = (
163
175
  limits: LtAccountLimits
164
176
  ): { maker: string; taker: string } => ({
@@ -784,7 +796,7 @@ export const createLighterProvider = (
784
796
  }
785
797
  }
786
798
 
787
- return {
799
+ const plugin: LighterPerpsProvider = {
788
800
  type: providerKey,
789
801
 
790
802
  internalSetupActions: [ActionType.SET_REFERRAL],
@@ -848,12 +860,12 @@ export const createLighterProvider = (
848
860
  )
849
861
 
850
862
  const totalMarginUsed = positions.reduce(
851
- (sum, p) => sum + Number.parseFloat(p.marginUsed),
852
- 0
863
+ (sum, p) => sum.plus(toRequiredBig(p.marginUsed, 'marginUsed')),
864
+ new Big(0)
853
865
  )
854
866
  const totalUnrealizedPnl = positions.reduce(
855
- (sum, p) => sum + Number.parseFloat(p.unrealizedPnl),
856
- 0
867
+ (sum, p) => sum.plus(toRequiredBig(p.unrealizedPnl, 'unrealizedPnl')),
868
+ new Big(0)
857
869
  )
858
870
 
859
871
  const instanceMeta = providers.find((p) => p.key === providerKey)
@@ -867,59 +879,75 @@ export const createLighterProvider = (
867
879
  account.available_balance,
868
880
  'available_balance'
869
881
  )
870
- // An underwater account reports no buying power. That is a margin
871
- // deficit, not a holding, so it carries no collateral row either.
872
- const collateralBalances: Balance[] = availableBalance.gt(0)
873
- ? [
874
- {
875
- categoryId: perpsCategory?.id ?? providerKey,
876
- asset:
877
- perpsCategory?.quoteAsset ??
878
- lighterAsset(
879
- collateral.displaySymbol,
880
- collateral.displaySymbol,
881
- providerKey
882
- ),
883
- units: availableBalance.toString(),
884
- valueUsd: availableBalance.toString(),
885
- price: '1',
886
- },
887
- ]
888
- : []
889
- // Spot token holdings — non-collateral. The instance's settlement asset
890
- // is valued 1:1; an asset no spot market prices keeps a zero USD value.
882
+ // Each asset has a spot route (`balance`) and a perps route
883
+ // (`margin_balance`). The instance's settlement asset is valued 1:1; an
884
+ // asset no spot market prices keeps a zero USD value.
891
885
  const heldAssets = account.assets.filter((asset) =>
892
886
  toRequiredBig(asset.balance, 'balance').gt(0)
893
887
  )
888
+ const marginAssets = account.assets.filter((asset) =>
889
+ toRequiredBig(asset.margin_balance, 'margin_balance').gt(0)
890
+ )
894
891
  const spotPrices = spotPriceByAssetId(
895
892
  registry.markets,
896
893
  spotCategoryId,
897
894
  prices,
898
895
  new Set(
899
- heldAssets
896
+ [...heldAssets, ...marginAssets]
900
897
  .filter((a) => a.asset_id !== collateral.assetIndex)
901
898
  .map((a) => String(a.asset_id))
902
899
  )
903
900
  )
904
- const balances: Balance[] = heldAssets.map((a) => {
905
- const assetId = String(a.asset_id)
901
+ const toBalance = (
902
+ a: LtAccountAsset,
903
+ categoryId: string,
904
+ asset: Asset,
905
+ units: string
906
+ ): Balance => {
906
907
  const price =
907
908
  a.asset_id === collateral.assetIndex
908
909
  ? new Big(1)
909
- : spotPrices.get(assetId)
910
+ : spotPrices.get(String(a.asset_id))
910
911
  return {
911
- categoryId: spotCategoryId,
912
- asset:
913
- assetRegistry.get(assetId) ??
914
- lighterAsset(assetId, a.symbol, providerKey),
915
- units: a.balance,
912
+ categoryId,
913
+ asset,
914
+ units,
916
915
  valueUsd:
917
- price === undefined
918
- ? '0'
919
- : toRequiredBig(a.balance, 'balance').times(price).toFixed(),
916
+ price === undefined ? '0' : new Big(units).times(price).toFixed(),
920
917
  ...(price === undefined ? {} : { price: price.toFixed() }),
921
918
  }
919
+ }
920
+ const registryAsset = (a: LtAccountAsset): Asset =>
921
+ assetRegistry.get(String(a.asset_id)) ??
922
+ lighterAsset(String(a.asset_id), a.symbol, providerKey)
923
+ // The perps category names the settlement asset by its quote asset, so
924
+ // the settlement row carries that descriptor rather than the spot one.
925
+ const settlementAsset =
926
+ perpsCategory?.quoteAsset ??
927
+ lighterAsset(
928
+ collateral.displaySymbol,
929
+ collateral.displaySymbol,
930
+ providerKey
931
+ )
932
+ // A category transfer moves only the settlement asset, capped by the
933
+ // account's free margin; every other perps-route asset releases none.
934
+ const collateralBalances: Balance[] = marginAssets.map((a) => {
935
+ const isSettlement = a.asset_id === collateral.assetIndex
936
+ return {
937
+ ...toBalance(
938
+ a,
939
+ perpsCategory?.id ?? providerKey,
940
+ isSettlement ? settlementAsset : registryAsset(a),
941
+ a.margin_balance
942
+ ),
943
+ transferable: isSettlement
944
+ ? transferableWithin(availableBalance, new Big(a.margin_balance))
945
+ : '0',
946
+ }
922
947
  })
948
+ const balances: Balance[] = heldAssets.map((a) =>
949
+ toBalance(a, spotCategoryId, registryAsset(a), a.balance)
950
+ )
923
951
 
924
952
  const assetCollateral = account.assets.flatMap((a) =>
925
953
  a.margin_mode === undefined
@@ -961,8 +989,8 @@ export const createLighterProvider = (
961
989
  balances,
962
990
  collateralBalances,
963
991
  positions,
964
- marginUsed: totalMarginUsed.toString(),
965
- unrealizedPnl: totalUnrealizedPnl.toString(),
992
+ marginUsed: totalMarginUsed.toFixed(),
993
+ unrealizedPnl: totalUnrealizedPnl.toFixed(),
966
994
  feeTier:
967
995
  limitsResult === undefined
968
996
  ? ZERO_FEE_TIER
@@ -1025,6 +1053,25 @@ export const createLighterProvider = (
1025
1053
  }
1026
1054
  },
1027
1055
 
1056
+ /**
1057
+ * Lighter publishes no per-market figure, so this derives one from the
1058
+ * account's `available_balance` and its open position on the market.
1059
+ */
1060
+ async getAvailableToTrade(
1061
+ params: ProviderGetAvailableToTradeParams,
1062
+ opts?: SDKRequestOptions
1063
+ ): Promise<AvailableToTrade | undefined> {
1064
+ const registry = getMarketRegistry(requireClient(), providerKey)
1065
+ await registry.sync()
1066
+ const market = registry.require(params.marketId)
1067
+ if (market.categoryId === LIGHTER_SPOT_CATEGORY_ID) {
1068
+ return undefined
1069
+ }
1070
+ const account = await plugin.getAccount({ address: params.address }, opts)
1071
+ const { availableMargin } = getAccountSummary(account, account.positions)
1072
+ return lighterAvailableToTrade(market, availableMargin, account.positions)
1073
+ },
1074
+
1028
1075
  /**
1029
1076
  * Lighter reports a market's margin mode and leverage only on the
1030
1077
  * account's position row, so a market the account never touched (or a
@@ -1695,6 +1742,7 @@ export const createLighterProvider = (
1695
1742
  )
1696
1743
  },
1697
1744
  }
1745
+ return plugin
1698
1746
  }
1699
1747
 
1700
1748
  /**
@@ -7,24 +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
28
  * Roll a Lighter account up into an {@link AccountSummary}. `totalAssetValue`
24
29
  * 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.
30
+ * every spot `balances` row, settlement included; the `collateralBalances` rows
31
+ * are the perps-route holding already inside `totalAssetValue`. The positions
32
+ * supply only the margin and PnL breakdown.
28
33
  *
29
34
  * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
30
35
  * @public
@@ -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
+ }
@@ -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
  )