@lifi/perps-sdk-provider-lighter 27.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 (60) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +20 -8
  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 +8 -4
  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/esm/LighterProvider.d.ts.map +1 -1
  23. package/dist/esm/LighterProvider.js +20 -12
  24. package/dist/esm/LighterProvider.js.map +1 -1
  25. package/dist/esm/accountSummary.d.ts +5 -4
  26. package/dist/esm/accountSummary.d.ts.map +1 -1
  27. package/dist/esm/accountSummary.js +13 -10
  28. package/dist/esm/accountSummary.js.map +1 -1
  29. package/dist/esm/utils/index.d.ts +1 -1
  30. package/dist/esm/utils/index.d.ts.map +1 -1
  31. package/dist/esm/utils/index.js +1 -1
  32. package/dist/esm/utils/index.js.map +1 -1
  33. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  34. package/dist/esm/utils/mapOrder.js +2 -1
  35. package/dist/esm/utils/mapOrder.js.map +1 -1
  36. package/dist/esm/utils/spotPrice.d.ts +14 -0
  37. package/dist/esm/utils/spotPrice.d.ts.map +1 -0
  38. package/dist/esm/utils/spotPrice.js +30 -0
  39. package/dist/esm/utils/spotPrice.js.map +1 -0
  40. package/dist/esm/utils/transferMargin.d.ts +11 -5
  41. package/dist/esm/utils/transferMargin.d.ts.map +1 -1
  42. package/dist/esm/utils/transferMargin.js +39 -11
  43. package/dist/esm/utils/transferMargin.js.map +1 -1
  44. package/dist/types/LighterProvider.d.ts.map +1 -1
  45. package/dist/types/accountSummary.d.ts +5 -4
  46. package/dist/types/accountSummary.d.ts.map +1 -1
  47. package/dist/types/utils/index.d.ts +1 -1
  48. package/dist/types/utils/index.d.ts.map +1 -1
  49. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  50. package/dist/types/utils/spotPrice.d.ts +14 -0
  51. package/dist/types/utils/spotPrice.d.ts.map +1 -0
  52. package/dist/types/utils/transferMargin.d.ts +11 -5
  53. package/dist/types/utils/transferMargin.d.ts.map +1 -1
  54. package/package.json +3 -3
  55. package/src/LighterProvider.ts +40 -20
  56. package/src/accountSummary.ts +16 -13
  57. package/src/utils/index.ts +1 -1
  58. package/src/utils/mapOrder.ts +2 -1
  59. package/src/utils/spotPrice.ts +39 -0
  60. package/src/utils/transferMargin.ts +52 -14
@@ -1 +1 @@
1
- {"version":3,"file":"transferMargin.d.ts","sourceRoot":"","sources":["../../../src/utils/transferMargin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,yBAAyB,EAE/B,MAAM,mBAAmB,CAAA;AAK1B;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,GACjB,yBAAyB,GAAG,SAAS,CAWvC"}
1
+ {"version":3,"file":"transferMargin.d.ts","sourceRoot":"","sources":["../../../src/utils/transferMargin.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,QAAQ,EAGd,MAAM,mBAAmB,CAAA;AA2B1B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,GACjB,MAAM,GAAG,SAAS,CAmBpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/perps-sdk-provider-lighter",
3
- "version": "27.0.0",
3
+ "version": "28.0.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": "^15.0.0"
47
+ "@lifi/perps-types": "^16.0.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@lifi/perps-sdk": "^16.0.0"
50
+ "@lifi/perps-sdk": "^17.0.0"
51
51
  },
52
52
  "publishConfig": {
53
53
  "access": "public"
@@ -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,
@@ -63,6 +64,7 @@ import {
63
64
  MarginMode,
64
65
  PerpsErrorCode,
65
66
  } from '@lifi/perps-types'
67
+ import Big from 'big.js'
66
68
  import type { Address } from 'viem'
67
69
  import { projectLighterConfigSettings } from './accountConfig.js'
68
70
  import { getAccountSummary } from './accountSummary.js'
@@ -138,7 +140,7 @@ import {
138
140
  mapOpenPositions,
139
141
  mapOrder,
140
142
  mapPortfolioHistory,
141
- positionMarginConstraints,
143
+ positionRemovableMargin,
142
144
  toBigOrNull,
143
145
  toIsoFromMs,
144
146
  toIsoFromSeconds,
@@ -148,6 +150,7 @@ import {
148
150
  fetchRegisteredApiKey,
149
151
  normalizeLighterPublicKey,
150
152
  } from './utils/registeredApiKey.js'
153
+ import { spotPriceByAssetId } from './utils/spotPrice.js'
151
154
  import { isPlaceholderTxHash } from './utils/txHash.js'
152
155
  import { wireList } from './utils/wireList.js'
153
156
 
@@ -825,11 +828,12 @@ export const createLighterProvider = (
825
828
 
826
829
  const registry = getMarketRegistry(requireClient(), providerKey)
827
830
  const assetRegistry = getAssetRegistry(requireClient(), providerKey)
828
- const [{ providers }, , , limitsResult, storedReadOnlyToken] =
831
+ const [{ providers }, , , { prices }, limitsResult, storedReadOnlyToken] =
829
832
  await Promise.all([
830
833
  getProviders(requireClient()),
831
834
  registry.sync(),
832
835
  assetRegistry.sync(),
836
+ getMarketsContext(requireClient(), { provider: providerKey }, opts),
833
837
  token === undefined
834
838
  ? Promise.resolve(undefined)
835
839
  : retryOnRevoked(opts, params.address, token, (resolvedToken) =>
@@ -883,23 +887,39 @@ export const createLighterProvider = (
883
887
  ]
884
888
  : []
885
889
  // 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
- })
890
+ // is valued 1:1; an asset no spot market prices keeps a zero USD value.
891
+ const heldAssets = account.assets.filter((asset) =>
892
+ toRequiredBig(asset.balance, 'balance').gt(0)
893
+ )
894
+ const spotPrices = spotPriceByAssetId(
895
+ registry.markets,
896
+ spotCategoryId,
897
+ prices,
898
+ new Set(
899
+ heldAssets
900
+ .filter((a) => a.asset_id !== collateral.assetIndex)
901
+ .map((a) => String(a.asset_id))
902
+ )
903
+ )
904
+ const balances: Balance[] = heldAssets.map((a) => {
905
+ const assetId = String(a.asset_id)
906
+ const price =
907
+ a.asset_id === collateral.assetIndex
908
+ ? new Big(1)
909
+ : spotPrices.get(assetId)
910
+ return {
911
+ categoryId: spotCategoryId,
912
+ asset:
913
+ assetRegistry.get(assetId) ??
914
+ lighterAsset(assetId, a.symbol, providerKey),
915
+ units: a.balance,
916
+ valueUsd:
917
+ price === undefined
918
+ ? '0'
919
+ : toRequiredBig(a.balance, 'balance').times(price).toFixed(),
920
+ ...(price === undefined ? {} : { price: price.toFixed() }),
921
+ }
922
+ })
903
923
 
904
924
  const assetCollateral = account.assets.flatMap((a) =>
905
925
  a.margin_mode === undefined
@@ -1603,7 +1623,7 @@ export const createLighterProvider = (
1603
1623
 
1604
1624
  estimateLiquidationPrice,
1605
1625
 
1606
- positionMarginConstraints,
1626
+ positionRemovableMargin,
1607
1627
 
1608
1628
  projectConfig(
1609
1629
  config: AccountConfig,
@@ -20,10 +20,11 @@ const lighterConfig = (account: AccountResponse): LighterAccountConfig => {
20
20
  }
21
21
 
22
22
  /**
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.
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.
27
28
  *
28
29
  * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
29
30
  * @public
@@ -41,18 +42,20 @@ export function getAccountSummary(
41
42
  unrealizedPnl = unrealizedPnl.plus(position.unrealizedPnl)
42
43
  }
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
+ )
50
+ }
51
+
44
52
  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(),
53
+ portfolioValue: portfolioValue.toFixed(),
51
54
  availableMargin: toRequiredBig(
52
55
  config.availableBalance,
53
56
  'availableBalance'
54
- ).toString(),
55
- marginUsed: marginUsed.toString(),
56
- unrealizedPnl: unrealizedPnl.toString(),
57
+ ).toFixed(),
58
+ marginUsed: marginUsed.toFixed(),
59
+ unrealizedPnl: unrealizedPnl.toFixed(),
57
60
  }
58
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
  }