@lifi/perps-sdk-provider-lighter 25.0.0 → 27.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 (68) hide show
  1. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  2. package/dist/cjs/LighterProvider.js +13 -11
  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 +11 -18
  6. package/dist/cjs/accountSummary.js.map +1 -1
  7. package/dist/cjs/constants.d.ts +2 -0
  8. package/dist/cjs/constants.d.ts.map +1 -1
  9. package/dist/cjs/constants.js +3 -0
  10. package/dist/cjs/constants.js.map +1 -1
  11. package/dist/cjs/utils/mapFill.d.ts.map +1 -1
  12. package/dist/cjs/utils/mapFill.js +4 -1
  13. package/dist/cjs/utils/mapFill.js.map +1 -1
  14. package/dist/cjs/utils/mapOrder.d.ts.map +1 -1
  15. package/dist/cjs/utils/mapOrder.js +3 -0
  16. package/dist/cjs/utils/mapOrder.js.map +1 -1
  17. package/dist/cjs/utils/txHash.d.ts +2 -0
  18. package/dist/cjs/utils/txHash.d.ts.map +1 -0
  19. package/dist/cjs/utils/txHash.js +10 -0
  20. package/dist/cjs/utils/txHash.js.map +1 -0
  21. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  22. package/dist/cjs/websocket/LighterWsProvider.js +11 -8
  23. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  24. package/dist/esm/LighterProvider.d.ts.map +1 -1
  25. package/dist/esm/LighterProvider.js +16 -17
  26. package/dist/esm/LighterProvider.js.map +1 -1
  27. package/dist/esm/accountSummary.d.ts +5 -6
  28. package/dist/esm/accountSummary.d.ts.map +1 -1
  29. package/dist/esm/accountSummary.js +19 -25
  30. package/dist/esm/accountSummary.js.map +1 -1
  31. package/dist/esm/constants.d.ts +9 -3
  32. package/dist/esm/constants.d.ts.map +1 -1
  33. package/dist/esm/constants.js +6 -3
  34. package/dist/esm/constants.js.map +1 -1
  35. package/dist/esm/types/ws.d.ts +1 -1
  36. package/dist/esm/utils/mapFill.d.ts.map +1 -1
  37. package/dist/esm/utils/mapFill.js +4 -1
  38. package/dist/esm/utils/mapFill.js.map +1 -1
  39. package/dist/esm/utils/mapOrder.d.ts.map +1 -1
  40. package/dist/esm/utils/mapOrder.js +3 -0
  41. package/dist/esm/utils/mapOrder.js.map +1 -1
  42. package/dist/esm/utils/txHash.d.ts +8 -0
  43. package/dist/esm/utils/txHash.d.ts.map +1 -0
  44. package/dist/esm/utils/txHash.js +18 -0
  45. package/dist/esm/utils/txHash.js.map +1 -0
  46. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  47. package/dist/esm/websocket/LighterWsProvider.js +19 -14
  48. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  49. package/dist/types/LighterProvider.d.ts.map +1 -1
  50. package/dist/types/accountSummary.d.ts +5 -6
  51. package/dist/types/accountSummary.d.ts.map +1 -1
  52. package/dist/types/constants.d.ts +9 -3
  53. package/dist/types/constants.d.ts.map +1 -1
  54. package/dist/types/types/ws.d.ts +1 -1
  55. package/dist/types/utils/mapFill.d.ts.map +1 -1
  56. package/dist/types/utils/mapOrder.d.ts.map +1 -1
  57. package/dist/types/utils/txHash.d.ts +8 -0
  58. package/dist/types/utils/txHash.d.ts.map +1 -0
  59. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  60. package/package.json +3 -3
  61. package/src/LighterProvider.ts +18 -23
  62. package/src/accountSummary.ts +30 -26
  63. package/src/constants.ts +11 -3
  64. package/src/types/ws.ts +1 -1
  65. package/src/utils/mapFill.ts +4 -1
  66. package/src/utils/mapOrder.ts +3 -0
  67. package/src/utils/txHash.ts +20 -0
  68. package/src/websocket/LighterWsProvider.ts +21 -15
@@ -1,53 +1,57 @@
1
+ import { PerpsError } from '@lifi/perps-sdk'
1
2
  import type {
2
3
  AccountResponse,
3
4
  AccountSummary,
5
+ LighterAccountConfig,
4
6
  Position,
5
7
  } from '@lifi/perps-types'
6
- import { MarginMode } from '@lifi/perps-types'
8
+ import { PerpsErrorCode } from '@lifi/perps-types'
7
9
  import Big from 'big.js'
10
+ import { toRequiredBig } from './utils/decimal.js'
11
+
12
+ const lighterConfig = (account: AccountResponse): LighterAccountConfig => {
13
+ if (account.config.provider !== 'lighter') {
14
+ throw new PerpsError(
15
+ PerpsErrorCode.SDKError,
16
+ `Lighter account summary received a '${account.config.provider}' account config`
17
+ )
18
+ }
19
+ return account.config
20
+ }
8
21
 
9
22
  /**
10
- * Roll a Lighter {@link AccountResponse} up into an {@link AccountSummary}.
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.
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.
16
27
  *
28
+ * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
17
29
  * @public
18
30
  */
19
31
  export function getAccountSummary(
20
32
  account: AccountResponse,
21
33
  positions: Position[]
22
34
  ): AccountSummary {
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
- }
35
+ const config = lighterConfig(account)
32
36
 
33
37
  let marginUsed = new Big(0)
34
38
  let unrealizedPnl = new Big(0)
35
- let isolatedUnrealizedPnl = new Big(0)
36
39
  for (const position of positions) {
37
40
  marginUsed = marginUsed.plus(position.marginUsed)
38
41
  unrealizedPnl = unrealizedPnl.plus(position.unrealizedPnl)
39
- if (position.marginMode === MarginMode.ISOLATED) {
40
- isolatedUnrealizedPnl = isolatedUnrealizedPnl.plus(position.unrealizedPnl)
41
- }
42
42
  }
43
43
 
44
44
  return {
45
- portfolioValue: nonCollateralValue
46
- .plus(availableMargin)
47
- .plus(marginUsed)
48
- .plus(isolatedUnrealizedPnl)
49
- .toString(),
50
- availableMargin: availableMargin.toString(),
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(),
51
+ availableMargin: toRequiredBig(
52
+ config.availableBalance,
53
+ 'availableBalance'
54
+ ).toString(),
51
55
  marginUsed: marginUsed.toString(),
52
56
  unrealizedPnl: unrealizedPnl.toString(),
53
57
  }
package/src/constants.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExplorerChainId } from '@lifi/perps-sdk'
1
2
  import type { FeeTier, LighterProviderKey } from '@lifi/perps-types'
2
3
  import { LT_ASSET_ID_USDC } from './types/action.js'
3
4
 
@@ -114,6 +115,11 @@ export interface LighterDeployment {
114
115
  * When omitted, transfer links are not emitted for this deployment.
115
116
  */
116
117
  explorerTxBaseUrl?: string
118
+ /**
119
+ * L1 the deployment's bridge settles on. Deposit and withdrawal rows report
120
+ * an `l1_tx_hash` on this chain, not on the zkLighter L2.
121
+ */
122
+ bridgeChainId: ExplorerChainId
117
123
  }
118
124
 
119
125
  /**
@@ -128,6 +134,7 @@ export const LIGHTER_MAINNET_DEPLOYMENT: LighterDeployment = Object.freeze({
128
134
  signerChainId: LIGHTER_MAINNET_SIGNER_CHAIN_ID,
129
135
  collateral: LIGHTER_COLLATERAL_ASSETS[LIGHTER_PROVIDER_KEY],
130
136
  explorerTxBaseUrl: DEFAULT_LIGHTER_EXPLORER_TX_BASE_URL,
137
+ bridgeChainId: ExplorerChainId.ETHEREUM,
131
138
  })
132
139
 
133
140
  /**
@@ -155,9 +162,9 @@ export const LIGHTER_RH_WS_URL = 'wss://api.rh.lighter.xyz/stream'
155
162
  export const LIGHTER_RH_SIGNER_CHAIN_ID = 466324
156
163
 
157
164
  /**
158
- * Lighter on Robinhood chain, as served by `lighterRhProvider()`. No explorer
159
- * base URL: the RH zkLighter explorer is unpublished, so transfer links are
160
- * left unset rather than pointed at the mainnet explorer.
165
+ * Lighter on Robinhood chain, as served by `lighterRhProvider()`. No L2
166
+ * explorer base URL: the RH zkLighter explorer is unpublished, so transfer
167
+ * links are left unset rather than pointed at the mainnet explorer.
161
168
  *
162
169
  * @public
163
170
  */
@@ -168,6 +175,7 @@ export const LIGHTER_RH_DEPLOYMENT: LighterDeployment = Object.freeze({
168
175
  signerChainId: LIGHTER_RH_SIGNER_CHAIN_ID,
169
176
  collateral: LIGHTER_COLLATERAL_ASSETS[LIGHTER_RH_PROVIDER_KEY],
170
177
  explorerTxBaseUrl: undefined,
178
+ bridgeChainId: ExplorerChainId.ROBINHOOD,
171
179
  })
172
180
 
173
181
  /** @internal */
package/src/types/ws.ts CHANGED
@@ -65,7 +65,7 @@ export type LtWsMarketStatsAllMessage = LtWsMessage & {
65
65
 
66
66
  /**
67
67
  * Spot market stats carry no funding/open-interest fields and address
68
- * markets by their spot `market_id` (2048+).
68
+ * markets by their `market_id`.
69
69
  * @public
70
70
  */
71
71
  export type LtWsSpotMarketStats = {
@@ -12,6 +12,7 @@ import {
12
12
  } from '../constants.js'
13
13
  import type { LtTrade } from '../types/index.js'
14
14
  import { leverageFromImf } from './mapPosition.js'
15
+ import { isPlaceholderTxHash } from './txHash.js'
15
16
 
16
17
  /**
17
18
  * Fee charged on a fill, in the market's quote asset. Lighter publishes the
@@ -156,6 +157,8 @@ export const mapFill = (
156
157
  trade.size
157
158
  ),
158
159
  createdAt: new Date(trade.timestamp).toISOString(),
159
- explorerLink: explorerTxUrl(ExplorerChainId.LIGHTER, trade.tx_hash),
160
+ explorerLink: isPlaceholderTxHash(trade.tx_hash)
161
+ ? undefined
162
+ : explorerTxUrl(ExplorerChainId.LIGHTER, trade.tx_hash),
160
163
  }
161
164
  }
@@ -61,6 +61,9 @@ const mapOrderStatus = (
61
61
  filledSize = new Big(order.filled_base_amount)
62
62
  ): OrderStatus => {
63
63
  switch (order.status) {
64
+ case 'pending':
65
+ case 'in-progress':
66
+ return OrderStatus.ACCEPTED
64
67
  case 'open':
65
68
  if (order.trigger_status === 'parent-order') {
66
69
  return OrderStatus.PENDING
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Trailing hex characters a Lighter placeholder `tx_hash` leaves zero. Lighter
3
+ * reports a 40-byte hash on every fill and transfer row, and a row that settled
4
+ * no transaction carries a sequence pair in the leading 16 bytes and zero in
5
+ * the remaining 24. A settled row fills all 40 bytes.
6
+ */
7
+ const PLACEHOLDER_ZERO_SUFFIX_LENGTH = 48
8
+
9
+ const ALL_ZERO = /^0+$/
10
+
11
+ /**
12
+ * Report whether a Lighter `tx_hash` is the placeholder the venue sends for a
13
+ * row that settled no transaction. Such a row resolves to no explorer link.
14
+ *
15
+ * @internal
16
+ */
17
+ export const isPlaceholderTxHash = (txHash: string | undefined): boolean =>
18
+ txHash !== undefined &&
19
+ txHash.length > PLACEHOLDER_ZERO_SUFFIX_LENGTH &&
20
+ ALL_ZERO.test(txHash.slice(-PLACEHOLDER_ZERO_SUFFIX_LENGTH))
@@ -29,6 +29,7 @@ import {
29
29
  DEFAULT_LIGHTER_WS_URL,
30
30
  LIGHTER_BASE_FEE_TIER,
31
31
  LIGHTER_PROVIDER_KEY,
32
+ LIGHTER_SPOT_CATEGORY_ID,
32
33
  } from '../constants.js'
33
34
  import type { LighterPerpsProvider } from '../LighterProvider.js'
34
35
  import type {
@@ -84,18 +85,19 @@ import {
84
85
  // Orderbook is stateful: the first message is a full snapshot, subsequent
85
86
  // messages are deltas where size=0 deletes a level.
86
87
 
87
- const LIGHTER_SPOT_MARKET_ID_OFFSET = 2048
88
-
89
88
  const LIGHTER_AUTH_CHANNEL = {
90
89
  orderUpdates: 'account_all_orders',
91
90
  fills: 'account_all_trades',
92
91
  positions: 'account_all_positions',
93
92
  } as const
94
93
 
95
- /** Channels whose handlers resolve market identity from the registry. */
94
+ /** Channels whose wire name or handler reads market identity from the registry. */
96
95
  function channelNeedsMarkets(channel: Subscription['channel']): boolean {
97
96
  return (
98
- channel === 'orderUpdates' || channel === 'fills' || channel === 'positions'
97
+ channel === 'marketContext' ||
98
+ channel === 'orderUpdates' ||
99
+ channel === 'fills' ||
100
+ channel === 'positions'
99
101
  )
100
102
  }
101
103
 
@@ -258,10 +260,10 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
258
260
  }
259
261
 
260
262
  protected async openChannel(sub: Subscription): Promise<() => void> {
261
- // Only the auth channels (orders/fills/positions) resolve markets.
262
- // `marketsContext`/`orderbook` are keyed purely by `String(market_id)`, so
263
- // gating them on the registry sync would let a failed `/markets` fetch kill
264
- // live price ticks.
263
+ // `marketsContext` resolves to two fixed `/all` channels and `orderbook` is
264
+ // keyed purely by `String(market_id)`, so neither needs the registry;
265
+ // gating them on the sync would let a failed `/markets` fetch kill live
266
+ // price ticks.
265
267
  if (channelNeedsMarkets(sub.channel)) {
266
268
  await this.registry?.sync()
267
269
  }
@@ -372,6 +374,7 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
372
374
  return `accountSummary:${sub.address.toLowerCase()}`
373
375
  case 'candle':
374
376
  case 'spotBalances':
377
+ case 'availableToTrade':
375
378
  throw new Error(`Lighter WS does not support channel: ${sub.channel}.`)
376
379
  }
377
380
  }
@@ -393,18 +396,21 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
393
396
  ]
394
397
  }
395
398
  if (sub.channel === 'marketContext') {
396
- const id = Number(sub.marketId)
397
- if (!Number.isFinite(id)) {
398
- throw new Error(
399
- `Lighter WS: unknown market for marketId '${sub.marketId}'. ` +
400
- 'MarketId must be a numeric market_id string.'
399
+ // Lighter market ids carry no type information, so the wire channel comes
400
+ // from the registry row's category, not from the numeric id.
401
+ if (this.registry === undefined) {
402
+ throw new PerpsError(
403
+ PerpsErrorCode.MarketNotFound,
404
+ `Lighter WS: no market registry for marketId '${sub.marketId}'. ` +
405
+ 'Construct via `lighterWsProvider({...})` and register with PerpsWsClient.'
401
406
  )
402
407
  }
408
+ const market = this.registry.require(sub.marketId)
403
409
  const prefix =
404
- id >= LIGHTER_SPOT_MARKET_ID_OFFSET
410
+ market.categoryId === LIGHTER_SPOT_CATEGORY_ID
405
411
  ? 'spot_market_stats'
406
412
  : 'market_stats'
407
- return [{ channel: `${prefix}/${id}`, needsAuth: false }]
413
+ return [{ channel: `${prefix}/${market.id}`, needsAuth: false }]
408
414
  }
409
415
  if (sub.channel === 'orderbook') {
410
416
  const id = Number(sub.marketId)