@lifi/perps-sdk-provider-lighter 31.0.0 → 32.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 (99) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  3. package/dist/cjs/LighterProvider.js +13 -9
  4. package/dist/cjs/LighterProvider.js.map +1 -1
  5. package/dist/cjs/accountSummary.d.ts +1 -1
  6. package/dist/cjs/accountSummary.d.ts.map +1 -1
  7. package/dist/cjs/accountSummary.js +8 -2
  8. package/dist/cjs/accountSummary.js.map +1 -1
  9. package/dist/cjs/constants.d.ts.map +1 -1
  10. package/dist/cjs/constants.js.map +1 -1
  11. package/dist/cjs/signers/LighterSigner.js +1 -1
  12. package/dist/cjs/signers/LighterSigner.js.map +1 -1
  13. package/dist/cjs/types/ws.d.ts +1 -1
  14. package/dist/cjs/types/ws.d.ts.map +1 -1
  15. package/dist/cjs/utils/index.d.ts +1 -1
  16. package/dist/cjs/utils/index.d.ts.map +1 -1
  17. package/dist/cjs/utils/index.js +3 -3
  18. package/dist/cjs/utils/index.js.map +1 -1
  19. package/dist/cjs/utils/liquidation.js +1 -1
  20. package/dist/cjs/utils/liquidation.js.map +1 -1
  21. package/dist/cjs/utils/mapMarketContext.d.ts.map +1 -1
  22. package/dist/cjs/utils/mapMarketContext.js +12 -4
  23. package/dist/cjs/utils/mapMarketContext.js.map +1 -1
  24. package/dist/cjs/utils/orderFormatting.d.ts +3 -3
  25. package/dist/cjs/utils/orderFormatting.d.ts.map +1 -1
  26. package/dist/cjs/utils/orderFormatting.js +5 -5
  27. package/dist/cjs/utils/orderFormatting.js.map +1 -1
  28. package/dist/cjs/utils/withdrawableBalances.d.ts.map +1 -1
  29. package/dist/cjs/utils/withdrawableBalances.js +16 -5
  30. package/dist/cjs/utils/withdrawableBalances.js.map +1 -1
  31. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  32. package/dist/cjs/websocket/LighterWsProvider.js +27 -7
  33. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  34. package/dist/esm/LighterProvider.d.ts.map +1 -1
  35. package/dist/esm/LighterProvider.js +18 -14
  36. package/dist/esm/LighterProvider.js.map +1 -1
  37. package/dist/esm/accountSummary.d.ts +4 -10
  38. package/dist/esm/accountSummary.d.ts.map +1 -1
  39. package/dist/esm/accountSummary.js +12 -12
  40. package/dist/esm/accountSummary.js.map +1 -1
  41. package/dist/esm/availableToTrade.d.ts +1 -1
  42. package/dist/esm/availableToTrade.js +1 -1
  43. package/dist/esm/constants.d.ts +4 -2
  44. package/dist/esm/constants.d.ts.map +1 -1
  45. package/dist/esm/constants.js +4 -2
  46. package/dist/esm/constants.js.map +1 -1
  47. package/dist/esm/signers/LighterSigner.js +2 -2
  48. package/dist/esm/signers/LighterSigner.js.map +1 -1
  49. package/dist/esm/types/ws.d.ts +2 -3
  50. package/dist/esm/types/ws.d.ts.map +1 -1
  51. package/dist/esm/utils/index.d.ts +1 -1
  52. package/dist/esm/utils/index.d.ts.map +1 -1
  53. package/dist/esm/utils/index.js +1 -1
  54. package/dist/esm/utils/index.js.map +1 -1
  55. package/dist/esm/utils/liquidation.js +1 -1
  56. package/dist/esm/utils/liquidation.js.map +1 -1
  57. package/dist/esm/utils/mapMarketContext.d.ts.map +1 -1
  58. package/dist/esm/utils/mapMarketContext.js +18 -4
  59. package/dist/esm/utils/mapMarketContext.js.map +1 -1
  60. package/dist/esm/utils/orderFormatting.d.ts +11 -11
  61. package/dist/esm/utils/orderFormatting.d.ts.map +1 -1
  62. package/dist/esm/utils/orderFormatting.js +12 -12
  63. package/dist/esm/utils/orderFormatting.js.map +1 -1
  64. package/dist/esm/utils/withdrawableBalances.d.ts.map +1 -1
  65. package/dist/esm/utils/withdrawableBalances.js +17 -6
  66. package/dist/esm/utils/withdrawableBalances.js.map +1 -1
  67. package/dist/esm/websocket/LighterWsProvider.d.ts +2 -12
  68. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  69. package/dist/esm/websocket/LighterWsProvider.js +27 -22
  70. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  71. package/dist/types/LighterProvider.d.ts.map +1 -1
  72. package/dist/types/accountSummary.d.ts +4 -10
  73. package/dist/types/accountSummary.d.ts.map +1 -1
  74. package/dist/types/availableToTrade.d.ts +1 -1
  75. package/dist/types/constants.d.ts +4 -2
  76. package/dist/types/constants.d.ts.map +1 -1
  77. package/dist/types/types/ws.d.ts +2 -3
  78. package/dist/types/types/ws.d.ts.map +1 -1
  79. package/dist/types/utils/index.d.ts +1 -1
  80. package/dist/types/utils/index.d.ts.map +1 -1
  81. package/dist/types/utils/mapMarketContext.d.ts.map +1 -1
  82. package/dist/types/utils/orderFormatting.d.ts +11 -11
  83. package/dist/types/utils/orderFormatting.d.ts.map +1 -1
  84. package/dist/types/utils/withdrawableBalances.d.ts.map +1 -1
  85. package/dist/types/websocket/LighterWsProvider.d.ts +2 -12
  86. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  87. package/package.json +3 -3
  88. package/src/LighterProvider.ts +36 -23
  89. package/src/accountSummary.ts +20 -13
  90. package/src/availableToTrade.ts +1 -1
  91. package/src/constants.ts +4 -2
  92. package/src/signers/LighterSigner.ts +2 -2
  93. package/src/types/ws.ts +4 -4
  94. package/src/utils/index.ts +1 -1
  95. package/src/utils/liquidation.ts +1 -1
  96. package/src/utils/mapMarketContext.ts +24 -4
  97. package/src/utils/orderFormatting.ts +22 -12
  98. package/src/utils/withdrawableBalances.ts +26 -11
  99. package/src/websocket/LighterWsProvider.ts +33 -31
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ACTIVE_ORDER_STATUSES,
3
+ calculateTransferable,
3
4
  type DepositFlow,
4
5
  explorerTxUrl,
5
6
  explorerTxUrlFromBase,
@@ -9,8 +10,6 @@ import {
9
10
  getProviders,
10
11
  isActiveOrderStatus,
11
12
  localStorageAdapter,
12
- maxOf,
13
- minOf,
14
13
  PerpsError,
15
14
  type PerpsProviderPlugin,
16
15
  type PerpsSDKClient,
@@ -66,6 +65,7 @@ import type {
66
65
  import {
67
66
  ActionType,
68
67
  ActivityType,
68
+ isDecimalString,
69
69
  MarginMode,
70
70
  PerpsErrorCode,
71
71
  } from '@lifi/perps-types'
@@ -142,8 +142,6 @@ import { isAssetMarginEnabled } from './utils/assetCollateral.js'
142
142
  import {
143
143
  estimateLiquidationPrice,
144
144
  fetchDetailedAccount,
145
- formatOrderPrice,
146
- formatOrderSize,
147
145
  leverageFromImf,
148
146
  lighterAsset,
149
147
  lighterWithdrawableBalances,
@@ -152,6 +150,8 @@ import {
152
150
  mapOrder,
153
151
  mapPortfolioHistory,
154
152
  positionRemovableMargin,
153
+ snapOrderPrice,
154
+ snapOrderSize,
155
155
  toBigOrNull,
156
156
  toIsoFromMs,
157
157
  toIsoFromSeconds,
@@ -1042,37 +1042,37 @@ export const createLighterProvider = (
1042
1042
  // account's free margin; every other perps-route asset releases none.
1043
1043
  const collateralBalances: Balance[] = marginAssets.map((a) => {
1044
1044
  const isSettlement = a.asset_id === collateral.assetIndex
1045
+ const marginBalance = toRequiredBig(
1046
+ a.margin_balance,
1047
+ 'margin_balance'
1048
+ ).toFixed()
1045
1049
  return {
1046
1050
  ...toBalance(
1047
1051
  a,
1048
1052
  perpsCategory?.id ?? providerKey,
1049
1053
  isSettlement ? settlementAsset : registryAsset(a),
1050
- a.margin_balance
1054
+ isDecimalString(a.margin_balance) ? a.margin_balance : marginBalance
1051
1055
  ),
1052
1056
  transferable: isSettlement
1053
- ? minOf(
1054
- maxOf(availableBalance, new Big(0)),
1055
- new Big(a.margin_balance)
1056
- ).toFixed()
1057
+ ? calculateTransferable(availableBalance.toFixed(), marginBalance)
1057
1058
  : '0',
1058
1059
  }
1059
1060
  })
1060
1061
  const balances: Balance[] = heldAssets.map((a) => {
1061
- const balance = new Big(a.balance)
1062
+ const balance = toRequiredBig(a.balance, 'balance')
1062
1063
  return {
1063
1064
  ...toBalance(
1064
1065
  a,
1065
1066
  LIGHTER_SPOT_CATEGORY_ID,
1066
1067
  registryAsset(a),
1067
- a.balance
1068
+ isDecimalString(a.balance) ? a.balance : balance.toFixed()
1068
1069
  ),
1069
- transferable: minOf(
1070
- maxOf(
1071
- balance.minus(toRequiredBig(a.locked_balance, 'locked_balance')),
1072
- new Big(0)
1073
- ),
1070
+ transferable: calculateTransferable(
1074
1071
  balance
1075
- ).toFixed(),
1072
+ .minus(toRequiredBig(a.locked_balance, 'locked_balance'))
1073
+ .toFixed(),
1074
+ balance.toFixed()
1075
+ ),
1076
1076
  }
1077
1077
  })
1078
1078
 
@@ -1192,8 +1192,8 @@ export const createLighterProvider = (
1192
1192
  },
1193
1193
 
1194
1194
  /**
1195
- * Lighter publishes no per-market figure, so this derives one from the
1196
- * account's `available_balance` and its open position on the market.
1195
+ * Derive per-market capacity from signed cross free collateral and the
1196
+ * margin released by closing the market's open position.
1197
1197
  */
1198
1198
  async getAvailableToTrade(
1199
1199
  params: ProviderGetAvailableToTradeParams,
@@ -1206,8 +1206,21 @@ export const createLighterProvider = (
1206
1206
  return undefined
1207
1207
  }
1208
1208
  const account = await plugin.getAccount({ address: params.address }, opts)
1209
- const { availableMargin } = getAccountSummary(account, account.positions)
1210
- return lighterAvailableToTrade(market, availableMargin, account.positions)
1209
+ const config = lighterConfig(account)
1210
+ const crossFreeCollateral = toRequiredBig(
1211
+ config.crossAssetValue,
1212
+ 'crossAssetValue'
1213
+ ).minus(
1214
+ toRequiredBig(
1215
+ config.crossInitialMarginRequirement,
1216
+ 'crossInitialMarginRequirement'
1217
+ )
1218
+ )
1219
+ return lighterAvailableToTrade(
1220
+ market,
1221
+ crossFreeCollateral.toFixed(),
1222
+ account.positions
1223
+ )
1211
1224
  },
1212
1225
 
1213
1226
  /**
@@ -1801,9 +1814,9 @@ export const createLighterProvider = (
1801
1814
  return getAccountSummary(account, positions)
1802
1815
  },
1803
1816
 
1804
- formatOrderPrice,
1817
+ snapOrderPrice,
1805
1818
 
1806
- formatOrderSize,
1819
+ snapOrderSize,
1807
1820
 
1808
1821
  estimateLiquidationPrice,
1809
1822
 
@@ -8,7 +8,11 @@ import type {
8
8
  import { PerpsErrorCode } from '@lifi/perps-types'
9
9
  import Big from 'big.js'
10
10
  import { atLeastZero } from './availableToTrade.js'
11
- import { LIGHTER_PROVIDER_KEY, LIGHTER_RH_PROVIDER_KEY } from './constants.js'
11
+ import {
12
+ LIGHTER_COLLATERAL_ASSETS,
13
+ LIGHTER_PROVIDER_KEY,
14
+ LIGHTER_RH_PROVIDER_KEY,
15
+ } from './constants.js'
12
16
  import { toRequiredBig } from './utils/decimal.js'
13
17
 
14
18
  /**
@@ -33,26 +37,20 @@ export const lighterConfig = (
33
37
  }
34
38
 
35
39
  /**
36
- * Lighter portfolio value: the perps-route equity (`total_asset_value`) plus
37
- * the USD value of every spot-route holding, settlement asset included.
40
+ * Add marked holdings excluded from Lighter's settlement equity.
38
41
  */
39
42
  export const lighterPortfolioValue = (
40
43
  perpsEquity: Big,
41
- spotValuesUsd: readonly string[]
44
+ holdingValuesUsd: readonly string[]
42
45
  ): Big =>
43
- spotValuesUsd.reduce(
46
+ holdingValuesUsd.reduce(
44
47
  (sum, valueUsd) => sum.plus(toRequiredBig(valueUsd, 'valueUsd')),
45
48
  perpsEquity
46
49
  )
47
50
 
48
51
  /**
49
- * Roll a Lighter account up into an {@link AccountSummary}. `totalAssetValue`
50
- * is perps-route equity and excludes the spot route, so `portfolioValue` adds
51
- * every spot `balances` row, settlement included; the `collateralBalances` rows
52
- * are the perps-route holding already inside `totalAssetValue`.
53
- * `availableMargin` is the cross free collateral, floored at 0: an isolated
54
- * position's equity stays with that position until the user removes it. The
55
- * positions supply only the margin and PnL breakdown.
52
+ * Roll up settlement equity, spot holdings and non-settlement margin holdings.
53
+ * Settlement margin is already included in `totalAssetValue`.
56
54
  *
57
55
  * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
58
56
  * @public
@@ -62,6 +60,15 @@ export function getAccountSummary(
62
60
  positions: Position[]
63
61
  ): AccountSummary {
64
62
  const config = lighterConfig(account)
63
+ const settlement = LIGHTER_COLLATERAL_ASSETS[config.provider]
64
+ const holdings = [
65
+ ...account.balances,
66
+ ...account.collateralBalances.filter(
67
+ ({ asset }) =>
68
+ asset.id !== String(settlement.assetIndex) &&
69
+ asset.id !== settlement.displaySymbol
70
+ ),
71
+ ]
65
72
 
66
73
  let marginUsed = new Big(0)
67
74
  let unrealizedPnl = new Big(0)
@@ -73,7 +80,7 @@ export function getAccountSummary(
73
80
  return {
74
81
  portfolioValue: lighterPortfolioValue(
75
82
  toRequiredBig(config.totalAssetValue, 'totalAssetValue'),
76
- account.balances.map((balance) => balance.valueUsd)
83
+ holdings.map((balance) => balance.valueUsd)
77
84
  ).toFixed(),
78
85
  availableMargin: atLeastZero(
79
86
  toRequiredBig(config.crossAssetValue, 'crossAssetValue').minus(
@@ -27,7 +27,7 @@ const releasedOnClose = (position: Position): Big => {
27
27
  * plus `availableMargin` and the margin that closing releases, floored at 0.
28
28
  * A deficit therefore never cuts the closing part. Both amounts are at least 0.
29
29
  *
30
- * @param availableMargin - The account's {@link AccountSummary.availableMargin}.
30
+ * @param availableMargin - Signed cross free collateral, before summary clamping.
31
31
  * @throws {PerpsError} `SDKError` when a decimal the formula reads is malformed.
32
32
  * @internal
33
33
  */
package/src/constants.ts CHANGED
@@ -18,8 +18,10 @@ export const LIGHTER_RH_PROVIDER_KEY = 'lighter-rh'
18
18
 
19
19
  /**
20
20
  * Category id for Lighter spot token holdings, matching the backend's Lighter
21
- * spot asset category. The perps category id is not a constant — it is read
22
- * from the fetched markets so it tracks the backend taxonomy.
21
+ * spot asset category. Every Lighter call site, REST and WebSocket, uses this
22
+ * constant for the spot category. The perps category id is not a constant:
23
+ * `LighterProvider.getAccount` reads it from the `/providers` category whose
24
+ * `quoteAsset` is not null.
23
25
  *
24
26
  * @public
25
27
  */
@@ -1,4 +1,4 @@
1
- import { PerpsError, scaleToInteger } from '@lifi/perps-sdk'
1
+ import { decimalToBaseUnits, PerpsError } from '@lifi/perps-sdk'
2
2
  import { ActionType, PerpsErrorCode } from '@lifi/perps-types'
3
3
  import Big from 'big.js'
4
4
  import { LT_ROUTE_PERP, LT_ROUTE_SPOT } from '../types/action.js'
@@ -632,7 +632,7 @@ export class LighterSigner {
632
632
  return wasm.SignWithdraw(
633
633
  numberField(p, 'asset_index'),
634
634
  routeType,
635
- scaleToInteger(amount, numberField(p, 'decimals'), 'truncate'),
635
+ decimalToBaseUnits(amount, numberField(p, 'decimals'), 'truncate'),
636
636
  SKIP_NONCE_DISABLED,
637
637
  nonce,
638
638
  ctx.apiKeyIndex,
package/src/types/ws.ts CHANGED
@@ -11,7 +11,7 @@ import type { LtAccountAsset } from './account.js'
11
11
  // - spot_market_stats/all → context across all spot markets
12
12
  // - spot_market_stats/{id} → context for one spot market
13
13
  // - order_book/{market_id} → bids/asks per market (stateful with deltas)
14
- // - account_all/{account} → per-asset spot-route balances
14
+ // - account_all/{account} → per-asset spot and margin-route balances
15
15
 
16
16
  /**
17
17
  * Common envelope fields on every Lighter WebSocket frame.
@@ -218,8 +218,7 @@ export type LtWsUserStatsMessage = LtWsMessage & {
218
218
  * `account_all/{account_index}` or `account_all_assets/{account_index}` frame,
219
219
  * read for its `assets` map only. The `subscribed/` frame carries every asset
220
220
  * keyed by asset index; an `update/` frame carries the changed assets, or
221
- * `null` when no balance changed. `balance` is the spot-route balance in asset
222
- * units.
221
+ * `null` when no balance changed. Route balances are in asset units.
223
222
  * @public
224
223
  */
225
224
  export type LtWsAccountAllMessage = LtWsMessage & {
@@ -230,7 +229,8 @@ export type LtWsAccountAllMessage = LtWsMessage & {
230
229
  | 'update/account_all_assets'
231
230
  assets?: Record<
232
231
  string,
233
- Pick<LtAccountAsset, 'symbol' | 'asset_id' | 'balance' | 'locked_balance'>
232
+ Pick<LtAccountAsset, 'symbol' | 'asset_id' | 'balance' | 'locked_balance'> &
233
+ Partial<Pick<LtAccountAsset, 'margin_balance'>>
234
234
  > | null
235
235
  }
236
236
 
@@ -21,7 +21,7 @@ export { mapPortfolioHistory } from './mapPortfolioHistory.js'
21
21
  export { leverageFromImf, mapPosition } from './mapPosition.js'
22
22
  export { mapMarketOrderLimits } from './marketLimits.js'
23
23
  export { mapInterval } from './ohlcvInterval.js'
24
- export { formatOrderPrice, formatOrderSize } from './orderFormatting.js'
24
+ export { snapOrderPrice, snapOrderSize } from './orderFormatting.js'
25
25
  export { toIsoFromMs, toIsoFromSeconds } from './time.js'
26
26
  export { positionRemovableMargin } from './transferMargin.js'
27
27
  export {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import {
11
- estimateIsolatedLiquidationPrice,
11
+ estimateLiquidationPrice as estimateIsolatedLiquidationPrice,
12
12
  type LiquidationEstimateParams,
13
13
  } from '@lifi/perps-sdk'
14
14
  import type { PerpsMarket } from '@lifi/perps-types'
@@ -1,3 +1,4 @@
1
+ import { numberToDecimalString } from '@lifi/perps-sdk'
1
2
  import type { MarketContext } from '@lifi/perps-types'
2
3
  import type { LtWsMarketStats, LtWsSpotMarketStats } from '../types/index.js'
3
4
 
@@ -7,6 +8,27 @@ const isPerpStats = (
7
8
  stats: LtWsMarketStats | LtWsSpotMarketStats
8
9
  ): stats is LtWsMarketStats => 'mark_price' in stats
9
10
 
11
+ const isFiniteFigure = (value: number | null | undefined): value is number =>
12
+ typeof value === 'number' && Number.isFinite(value)
13
+
14
+ /**
15
+ * The 24h figures of one record, omitted where the frame carries no finite
16
+ * number. The venue publishes them as unvalidated JSON, and `market_stats/all`
17
+ * maps every market in one pass, so a single absent figure must not cost the
18
+ * whole frame.
19
+ */
20
+ const dailyFigures = (stats: {
21
+ daily_price_change?: number | null
22
+ daily_quote_token_volume?: number | null
23
+ }): Pick<MarketContext, 'priceChange24h' | 'volume24h'> => ({
24
+ ...(isFiniteFigure(stats.daily_price_change)
25
+ ? { priceChange24h: numberToDecimalString(stats.daily_price_change) }
26
+ : {}),
27
+ ...(isFiniteFigure(stats.daily_quote_token_volume)
28
+ ? { volume24h: numberToDecimalString(stats.daily_quote_token_volume) }
29
+ : {}),
30
+ })
31
+
10
32
  /**
11
33
  * Map a Lighter market-stats record to a {@link MarketContext}: `index_price`
12
34
  * is the oracle, `mid_price` the mid. Perp records carry the venue
@@ -24,8 +46,7 @@ export const mapMarketContext = (
24
46
  midPrice: stats.mid_price,
25
47
  markPrice: stats.mark_price,
26
48
  oraclePrice: stats.index_price,
27
- priceChange24h: String(stats.daily_price_change),
28
- volume24h: String(stats.daily_quote_token_volume),
49
+ ...dailyFigures(stats),
29
50
  openInterest: stats.open_interest,
30
51
  funding: {
31
52
  rate: stats.current_funding_rate,
@@ -38,7 +59,6 @@ export const mapMarketContext = (
38
59
  midPrice: stats.mid_price,
39
60
  markPrice: stats.mid_price,
40
61
  oraclePrice: stats.index_price,
41
- priceChange24h: String(stats.daily_price_change),
42
- volume24h: String(stats.daily_quote_token_volume),
62
+ ...dailyFigures(stats),
43
63
  }
44
64
  }
@@ -1,31 +1,38 @@
1
1
  /**
2
- * Lighter order size and price formatting for exchange submission.
2
+ * Lighter order size and price snapping for exchange submission.
3
3
  *
4
4
  * Lighter publishes a flat per-market decimal budget on its order books
5
5
  * (`supported_price_decimals` / `supported_size_decimals`), surfaced on the
6
- * generic {@link Market} as `priceDecimals` / `szDecimals`. Prices round to
7
- * the tick grid; sizes truncate to the lot grid.
6
+ * generic {@link Market} as `priceDecimals` / `szDecimals`. Prices round onto
7
+ * the tick grid; sizes truncate onto the lot grid.
8
8
  */
9
9
 
10
10
  import { PerpsError } from '@lifi/perps-sdk'
11
- import { type Market, PerpsErrorCode } from '@lifi/perps-types'
11
+ import {
12
+ type DecimalString,
13
+ type Market,
14
+ PerpsErrorCode,
15
+ } from '@lifi/perps-types'
12
16
  import Big from 'big.js'
13
17
 
14
18
  /**
15
- * Format a price onto a Lighter market's tick grid: rounded to the market's
16
- * `priceDecimals` (Lighter's `supported_price_decimals`), trailing zeros
17
- * stripped.
19
+ * Snap a price onto a Lighter market's tick grid: rounded half-up to the
20
+ * market's `priceDecimals` (Lighter's `supported_price_decimals`), trailing
21
+ * zeros stripped.
18
22
  *
19
23
  * @throws {PerpsError} `ValidationError` when `market.priceDecimals` is
20
24
  * absent — without the venue's tick grid no correct price can be produced.
21
25
  * @public
22
26
  */
23
- export function formatOrderPrice(market: Market, price: number): string {
27
+ export function snapOrderPrice(
28
+ market: Market,
29
+ price: DecimalString
30
+ ): DecimalString {
24
31
  if (market.priceDecimals === undefined) {
25
32
  throw new PerpsError(
26
33
  PerpsErrorCode.ValidationError,
27
34
  `Market '${market.id}' carries no priceDecimals; Lighter order prices ` +
28
- `cannot be formatted without the market's tick grid.`
35
+ `cannot be snapped without the market's tick grid.`
29
36
  )
30
37
  }
31
38
  const rounded = new Big(price).round(market.priceDecimals, Big.roundHalfUp)
@@ -33,14 +40,17 @@ export function formatOrderPrice(market: Market, price: number): string {
33
40
  }
34
41
 
35
42
  /**
36
- * Format a size onto a Lighter market's lot grid: truncated (never rounded
37
- * up, so the size cannot exceed the user's balance) to the market's
43
+ * Snap a size onto a Lighter market's lot grid: truncated toward zero (never
44
+ * rounded up, so the size cannot exceed the user's balance) to the market's
38
45
  * `szDecimals` (Lighter's `supported_size_decimals`), trailing zeros stripped.
39
46
  *
40
47
  * @param size - Size in base-asset units as a non-negative magnitude.
41
48
  * @public
42
49
  */
43
- export function formatOrderSize(market: Market, size: number): string {
50
+ export function snapOrderSize(
51
+ market: Market,
52
+ size: DecimalString
53
+ ): DecimalString {
44
54
  const truncated = new Big(size).round(market.szDecimals, Big.roundDown)
45
55
  return truncated.eq(0) ? '0' : truncated.toFixed()
46
56
  }
@@ -1,4 +1,8 @@
1
- import { maxOf, minOf, type ProviderWithdrawableBalance } from '@lifi/perps-sdk'
1
+ import {
2
+ calculateTransferable,
3
+ calculateWithdrawMax,
4
+ type ProviderWithdrawableBalance,
5
+ } from '@lifi/perps-sdk'
2
6
  import Big from 'big.js'
3
7
  import type { LtAccount } from '../types/account.js'
4
8
  import { toRequiredBig } from './decimal.js'
@@ -29,21 +33,32 @@ export const lighterWithdrawableBalances = (
29
33
  toRequiredBig(asset.locked_balance, 'locked_balance')
30
34
  )
31
35
  if (spot.gt(0)) {
32
- rows.push({ assetId, route: 'spot', available: spot.toFixed() })
36
+ const available = spot.toFixed()
37
+ rows.push({
38
+ assetId,
39
+ route: 'spot',
40
+ available,
41
+ max: calculateWithdrawMax({ available }),
42
+ })
33
43
  }
34
44
  const marginBalance = toRequiredBig(asset.margin_balance, 'margin_balance')
35
45
  const perps =
36
46
  asset.asset_id === settlementAssetIndex
37
- ? minOf(
38
- maxOf(
39
- toRequiredBig(account.available_balance, 'available_balance'),
40
- new Big(0)
41
- ),
42
- marginBalance
47
+ ? calculateTransferable(
48
+ toRequiredBig(
49
+ account.available_balance,
50
+ 'available_balance'
51
+ ).toFixed(),
52
+ marginBalance.toFixed()
43
53
  )
44
- : marginBalance
45
- if (perps.gt(0)) {
46
- rows.push({ assetId, route: 'perps', available: perps.toFixed() })
54
+ : marginBalance.toFixed()
55
+ if (new Big(perps).gt(0)) {
56
+ rows.push({
57
+ assetId,
58
+ route: 'perps',
59
+ available: perps,
60
+ max: calculateWithdrawMax({ available: perps }),
61
+ })
47
62
  }
48
63
  }
49
64
  return rows
@@ -24,6 +24,7 @@ import {
24
24
  type Position,
25
25
  type Subscription,
26
26
  } from '@lifi/perps-types'
27
+ import Big from 'big.js'
27
28
  import type { Address } from 'viem'
28
29
  import { lighterPortfolioValue } from '../accountSummary.js'
29
30
  import {
@@ -148,14 +149,10 @@ interface BookLevel {
148
149
  priceNum: number
149
150
  }
150
151
 
151
- /**
152
- * Per-address inputs of the `accountSummary` emit: the latest `user_stats`
153
- * figures (`perps`, whose `equity` is perps-route only) and the spot-route
154
- * balance per asset index from `account_all` or `account_all_assets`.
155
- */
152
+ /** Settlement equity and per-asset route quantities for account valuation. */
156
153
  interface AccountSummaryInputs {
157
154
  perps?: { equity: Big } & Omit<AccountSummary, 'portfolioValue'>
158
- spotBalances?: Map<number, Big>
155
+ balances?: Map<number, { spot: Big; margin: Big }>
159
156
  lastPortfolioValue?: string
160
157
  }
161
158
 
@@ -516,9 +513,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
516
513
  // instead of every later frame.
517
514
  this.settlementAssetIndex()
518
515
  const accountIndex = await this.resolveAccountIndex(sub.address)
519
- // `account_all_assets` carries only the spot balances but rejects a
520
- // subscribe without auth; the public `account_all` also carries the
521
- // account's trades, positions and funding.
516
+ // `account_all_assets` requires auth; `account_all` is publicly readable.
522
517
  const hasToken = await this.canResolveAuthToken(sub.address)
523
518
  const assetsChannel = hasToken ? 'account_all_assets' : 'account_all'
524
519
  return [
@@ -785,12 +780,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
785
780
  this.emitAccountSummary(address)
786
781
  }
787
782
 
788
- /**
789
- * Replace the spot balances on a `subscribed/` frame and upsert them on an
790
- * `update/` frame. An update before the first snapshot is dropped, so the
791
- * map never holds a partial set. Balances parse here, so a non-decimal
792
- * balance rejects its frame and never reaches the emit path.
793
- */
783
+ /** Snapshots replace all route quantities; deltas upsert changed assets. */
794
784
  private handleAccountAll(
795
785
  msg: LtWsAccountAllMessage,
796
786
  prefix: 'account_all' | 'account_all_assets',
@@ -804,34 +794,46 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
804
794
  if (inputs === undefined || (!isSnapshot && !msg.assets)) {
805
795
  return
806
796
  }
807
- const parsed = Object.values(msg.assets ?? {}).map(
808
- (asset) =>
809
- [asset.asset_id, toRequiredBig(asset.balance, 'balance')] as const
810
- )
811
- const balances = isSnapshot ? new Map<number, Big>() : inputs.spotBalances
797
+ const parsed = Object.values(msg.assets ?? {}).map((asset) => ({
798
+ assetId: asset.asset_id,
799
+ spot: toRequiredBig(asset.balance, 'balance'),
800
+ margin:
801
+ asset.margin_balance === undefined
802
+ ? undefined
803
+ : toRequiredBig(asset.margin_balance, 'margin_balance'),
804
+ }))
805
+ const balances = isSnapshot
806
+ ? new Map<number, { spot: Big; margin: Big }>()
807
+ : inputs.balances
812
808
  if (balances === undefined) {
813
809
  return
814
810
  }
815
- for (const [assetId, balance] of parsed) {
816
- balances.set(assetId, balance)
811
+ for (const { assetId, spot, margin } of parsed) {
812
+ balances.set(assetId, {
813
+ spot,
814
+ margin: margin ?? balances.get(assetId)?.margin ?? new Big(0),
815
+ })
817
816
  }
818
- inputs.spotBalances = balances
817
+ inputs.balances = balances
819
818
  this.emitAccountSummary(address)
820
819
  }
821
820
 
822
- /**
823
- * Emit the account summary once both the `user_stats` figures and the spot
824
- * balances are in. `portfolioValue` values the spot balances as REST
825
- * `getAccount` does. `onlyOnChange` skips an emit whose `portfolioValue`
826
- * equals the last one, so a mark tick on an unrelated market emits nothing.
827
- */
821
+ /** Unrelated mark ticks do not emit an unchanged portfolio value. */
828
822
  private emitAccountSummary(address: string, onlyOnChange = false): void {
829
823
  const inputs = this.accountSummaryInputs.get(address)
830
- if (inputs?.perps === undefined || inputs.spotBalances === undefined) {
824
+ if (inputs?.perps === undefined || inputs.balances === undefined) {
831
825
  return
832
826
  }
833
827
  const settlementAssetIndex = this.settlementAssetIndex()
834
- const held = [...inputs.spotBalances].filter(([, balance]) => balance.gt(0))
828
+ const held = [...inputs.balances]
829
+ .map(
830
+ ([assetId, { spot, margin }]) =>
831
+ [
832
+ assetId,
833
+ assetId === settlementAssetIndex ? spot : spot.plus(margin),
834
+ ] as const
835
+ )
836
+ .filter(([, balance]) => balance.gt(0))
835
837
  const spotPrices = spotPriceByAssetId(
836
838
  this.registry?.markets ?? [],
837
839
  LIGHTER_SPOT_CATEGORY_ID,