@lifi/perps-sdk-provider-lighter 1.9.0 → 2.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 (44) hide show
  1. package/dist/cjs/accountSummary.js +1 -1
  2. package/dist/cjs/accountSummary.js.map +1 -1
  3. package/dist/cjs/signers/LighterSigner.d.ts.map +1 -1
  4. package/dist/cjs/signers/LighterSigner.js +23 -0
  5. package/dist/cjs/signers/LighterSigner.js.map +1 -1
  6. package/dist/cjs/signers/signActions.d.ts.map +1 -1
  7. package/dist/cjs/signers/signActions.js +14 -8
  8. package/dist/cjs/signers/signActions.js.map +1 -1
  9. package/dist/cjs/types/ws.d.ts +7 -0
  10. package/dist/cjs/types/ws.d.ts.map +1 -1
  11. package/dist/cjs/websocket/LighterWsProvider.d.ts +1 -0
  12. package/dist/cjs/websocket/LighterWsProvider.d.ts.map +1 -1
  13. package/dist/cjs/websocket/LighterWsProvider.js +41 -0
  14. package/dist/cjs/websocket/LighterWsProvider.js.map +1 -1
  15. package/dist/esm/accountSummary.d.ts +3 -3
  16. package/dist/esm/accountSummary.js +4 -4
  17. package/dist/esm/accountSummary.js.map +1 -1
  18. package/dist/esm/signers/LighterSigner.d.ts.map +1 -1
  19. package/dist/esm/signers/LighterSigner.js +32 -0
  20. package/dist/esm/signers/LighterSigner.js.map +1 -1
  21. package/dist/esm/signers/signActions.d.ts +7 -5
  22. package/dist/esm/signers/signActions.d.ts.map +1 -1
  23. package/dist/esm/signers/signActions.js +21 -13
  24. package/dist/esm/signers/signActions.js.map +1 -1
  25. package/dist/esm/types/ws.d.ts +11 -1
  26. package/dist/esm/types/ws.d.ts.map +1 -1
  27. package/dist/esm/websocket/LighterWsProvider.d.ts +1 -0
  28. package/dist/esm/websocket/LighterWsProvider.d.ts.map +1 -1
  29. package/dist/esm/websocket/LighterWsProvider.js +44 -0
  30. package/dist/esm/websocket/LighterWsProvider.js.map +1 -1
  31. package/dist/types/accountSummary.d.ts +3 -3
  32. package/dist/types/signers/LighterSigner.d.ts.map +1 -1
  33. package/dist/types/signers/signActions.d.ts +7 -5
  34. package/dist/types/signers/signActions.d.ts.map +1 -1
  35. package/dist/types/types/ws.d.ts +11 -1
  36. package/dist/types/types/ws.d.ts.map +1 -1
  37. package/dist/types/websocket/LighterWsProvider.d.ts +1 -0
  38. package/dist/types/websocket/LighterWsProvider.d.ts.map +1 -1
  39. package/package.json +3 -3
  40. package/src/accountSummary.ts +4 -4
  41. package/src/signers/LighterSigner.ts +54 -0
  42. package/src/signers/signActions.ts +21 -13
  43. package/src/types/ws.ts +12 -1
  44. package/src/websocket/LighterWsProvider.ts +49 -0
@@ -310,11 +310,13 @@ async function requireApiKey(
310
310
  * remaining legs. Each step's `txParams` carries chainId, target, function
311
311
  * name, args, and a human-readable abi from the backend.
312
312
  *
313
- * The wallet MUST already be on the leg's `txParams.chainId`: this signer
314
- * broadcasts on the wallet's active chain and never switches it (switching is
315
- * the integrator's responsibility — the widget drives it before signing). A leg
316
- * whose target chain differs from the wallet's chain throws before broadcasting,
317
- * so a token approve/deposit can never land on an unintended chain.
313
+ * Before broadcasting a leg the SDK switches the wallet to the leg's
314
+ * `txParams.chainId` via `ctx.switchToChain`, re-reading the returned client so
315
+ * the broadcast lands on the right chain even when legs target different chains.
316
+ * When no switch capability is supplied (local/private-key signer, or no
317
+ * `switchChain` hook configured) a leg whose target chain differs from the
318
+ * wallet's active chain throws before broadcasting, so a token approve/deposit
319
+ * can never land on an unintended chain.
318
320
  * @internal
319
321
  */
320
322
  export async function signEvmTxActions(
@@ -330,30 +332,36 @@ export async function signEvmTxActions(
330
332
  )
331
333
  }
332
334
 
335
+ const switchToChain = ctx?.switchToChain
333
336
  const signed: EvmTxSignedActionStep[] = []
337
+ let legSigner = walletSigner
334
338
  for (const step of steps) {
335
339
  const params = step.txParams
336
340
 
337
- if (walletSigner.chain?.id !== params.chainId) {
341
+ if (legSigner.chain?.id !== params.chainId && switchToChain) {
342
+ legSigner = await switchToChain(params.chainId)
343
+ }
344
+ if (legSigner.chain?.id !== params.chainId) {
338
345
  throw new PerpsError(
339
346
  PerpsErrorCode.SDKError,
340
347
  `EVM_TX leg '${step.action}' targets chain ${params.chainId}, but the ` +
341
- `connected wallet is on chain ${walletSigner.chain?.id ?? 'unknown'}. ` +
342
- `Switch the wallet to chain ${params.chainId} before signing — the ` +
343
- "SDK broadcasts on the wallet's active chain and does not switch it."
348
+ `connected wallet is on chain ${legSigner.chain?.id ?? 'unknown'} and ` +
349
+ 'no chain switch was performed. Configure a switchChain hook via ' +
350
+ `setSwitchChain, or connect the wallet to chain ${params.chainId} ` +
351
+ 'before signing.'
344
352
  )
345
353
  }
346
354
 
347
- const txHash = await walletSigner.writeContract({
355
+ const txHash = await legSigner.writeContract({
348
356
  address: params.to,
349
357
  abi: parseAbi(params.abi),
350
358
  functionName: params.functionName,
351
359
  args: params.args,
352
- chain: walletSigner.chain,
353
- account: walletSigner.account,
360
+ chain: legSigner.chain,
361
+ account: legSigner.account,
354
362
  })
355
363
 
356
- const receipt = await waitForTransactionReceipt(walletSigner, {
364
+ const receipt = await waitForTransactionReceipt(legSigner, {
357
365
  hash: txHash,
358
366
  })
359
367
  if (receipt.status === 'reverted') {
package/src/types/ws.ts CHANGED
@@ -149,7 +149,18 @@ export type LtWsAccountAllTradesMessage = LtWsMessage & {
149
149
  data?: { trades?: Record<string, unknown[]> | unknown[] }
150
150
  }
151
151
 
152
- /** @public */
152
+ /**
153
+ * `user_stats/{account_index}` frame; decimal strings in USD.
154
+ * @public
155
+ */
156
+ export type LtWsUserStatsMessage = LtWsMessage & {
157
+ stats?: {
158
+ collateral?: string
159
+ portfolio_value?: string
160
+ available_balance?: string
161
+ }
162
+ }
163
+
153
164
  export type LtWsAccountAllPositionsMessage = LtWsMessage & {
154
165
  type: 'subscribed/account_all_positions' | 'update/account_all_positions'
155
166
  positions?: Record<string, unknown>
@@ -40,6 +40,7 @@ import type {
40
40
  LtWsSpotMarketStats,
41
41
  LtWsSpotMarketStatsAllMessage,
42
42
  LtWsTradeMessage,
43
+ LtWsUserStatsMessage,
43
44
  } from '../types/index.js'
44
45
  import { LIGHTER_RETRY_DEFAULTS, LighterApiClient } from '../utils/apiClient.js'
45
46
  import {
@@ -310,6 +311,8 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
310
311
  return `fills:${sub.address.toLowerCase()}`
311
312
  case 'positions':
312
313
  return `positions:${sub.address.toLowerCase()}`
314
+ case 'accountSummary':
315
+ return `accountSummary:${sub.address.toLowerCase()}`
313
316
  // No live wire sub (openChannel returns a no-op), but a stable key is
314
317
  // still needed so the base's fan-out registry can track/release it.
315
318
  case 'candle':
@@ -369,6 +372,17 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
369
372
  }
370
373
  return [{ channel: `trade/${id}`, needsAuth: false }]
371
374
  }
375
+ if (sub.channel === 'accountSummary') {
376
+ const accountIndex = await this.resolveAccountIndex(sub.address)
377
+ // `user_stats` is publicly readable; no token needed.
378
+ return [
379
+ {
380
+ channel: `user_stats/${accountIndex}`,
381
+ needsAuth: false,
382
+ address: sub.address,
383
+ },
384
+ ]
385
+ }
372
386
  if (
373
387
  sub.channel === 'orderUpdates' ||
374
388
  sub.channel === 'fills' ||
@@ -503,6 +517,14 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
503
517
  return
504
518
  }
505
519
 
520
+ if (
521
+ msg.type === 'subscribed/user_stats' ||
522
+ msg.type === 'update/user_stats'
523
+ ) {
524
+ this.handleUserStats(msg as LtWsUserStatsMessage)
525
+ return
526
+ }
527
+
506
528
  if (
507
529
  msg.type === 'subscribed/account_all_positions' ||
508
530
  msg.type === 'update/account_all_positions'
@@ -531,6 +553,33 @@ export class LighterWsProvider extends WsProviderBase<SubState> {
531
553
  })
532
554
  }
533
555
 
556
+ private handleUserStats(msg: LtWsUserStatsMessage): void {
557
+ const address = this.addressFromChannel(msg.channel, 'user_stats')
558
+ if (!address) {
559
+ return
560
+ }
561
+ const stats = msg.stats
562
+ const portfolio = Number.parseFloat(stats?.portfolio_value ?? '')
563
+ const available = Number.parseFloat(stats?.available_balance ?? '')
564
+ if (!Number.isFinite(portfolio) || !Number.isFinite(available)) {
565
+ return
566
+ }
567
+ // Identities of the REST 'net' semantics: `available_balance` nets locked
568
+ // margin out with PnL marked in, and `collateral` is gross, PnL-exclusive.
569
+ const collateral = Number.parseFloat(stats?.collateral ?? '')
570
+ this.emit(`accountSummary:${address}`, {
571
+ channel: 'accountSummary',
572
+ data: {
573
+ portfolioValue: portfolio.toString(),
574
+ availableMargin: available.toString(),
575
+ marginUsed: Math.max(portfolio - available, 0).toString(),
576
+ unrealizedPnl: Number.isFinite(collateral)
577
+ ? (portfolio - collateral).toString()
578
+ : '0',
579
+ },
580
+ })
581
+ }
582
+
534
583
  private handleAccountTrades(msg: LtWsAccountAllTradesMessage): void {
535
584
  const address = this.addressFromChannel(msg.channel, 'account_all_trades')
536
585
  if (!address) {