@lifi/perps-sdk-provider-lighter 34.0.0 → 35.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 (42) hide show
  1. package/dist/cjs/LighterProvider.d.ts +0 -1
  2. package/dist/cjs/LighterProvider.d.ts.map +1 -1
  3. package/dist/cjs/LighterProvider.js +50 -44
  4. package/dist/cjs/LighterProvider.js.map +1 -1
  5. package/dist/cjs/accountSummary.d.ts.map +1 -1
  6. package/dist/cjs/accountSummary.js +3 -6
  7. package/dist/cjs/accountSummary.js.map +1 -1
  8. package/dist/cjs/signers/createAuthToken.d.ts.map +1 -1
  9. package/dist/cjs/signers/createAuthToken.js.map +1 -1
  10. package/dist/cjs/utils/settlementPool.d.ts +6 -0
  11. package/dist/cjs/utils/settlementPool.d.ts.map +1 -0
  12. package/dist/cjs/utils/settlementPool.js +21 -0
  13. package/dist/cjs/utils/settlementPool.js.map +1 -0
  14. package/dist/esm/LighterProvider.d.ts +7 -11
  15. package/dist/esm/LighterProvider.d.ts.map +1 -1
  16. package/dist/esm/LighterProvider.js +60 -51
  17. package/dist/esm/LighterProvider.js.map +1 -1
  18. package/dist/esm/accountSummary.d.ts +2 -1
  19. package/dist/esm/accountSummary.d.ts.map +1 -1
  20. package/dist/esm/accountSummary.js +5 -7
  21. package/dist/esm/accountSummary.js.map +1 -1
  22. package/dist/esm/signers/createAuthToken.d.ts +1 -2
  23. package/dist/esm/signers/createAuthToken.d.ts.map +1 -1
  24. package/dist/esm/signers/createAuthToken.js +1 -2
  25. package/dist/esm/signers/createAuthToken.js.map +1 -1
  26. package/dist/esm/utils/settlementPool.d.ts +14 -0
  27. package/dist/esm/utils/settlementPool.d.ts.map +1 -0
  28. package/dist/esm/utils/settlementPool.js +23 -0
  29. package/dist/esm/utils/settlementPool.js.map +1 -0
  30. package/dist/types/LighterProvider.d.ts +7 -11
  31. package/dist/types/LighterProvider.d.ts.map +1 -1
  32. package/dist/types/accountSummary.d.ts +2 -1
  33. package/dist/types/accountSummary.d.ts.map +1 -1
  34. package/dist/types/signers/createAuthToken.d.ts +1 -2
  35. package/dist/types/signers/createAuthToken.d.ts.map +1 -1
  36. package/dist/types/utils/settlementPool.d.ts +14 -0
  37. package/dist/types/utils/settlementPool.d.ts.map +1 -0
  38. package/package.json +3 -3
  39. package/src/LighterProvider.ts +98 -90
  40. package/src/accountSummary.ts +10 -10
  41. package/src/signers/createAuthToken.ts +1 -2
  42. package/src/utils/settlementPool.ts +36 -0
@@ -125,6 +125,7 @@ import type {
125
125
  } from './types/index.js'
126
126
  import {
127
127
  LT_ACCOUNT_TRADING_MODE_SIMPLE,
128
+ LT_ACCOUNT_TRADING_MODE_UNIFIED,
128
129
  LT_MARGIN_MODE_CROSS,
129
130
  LT_MARGIN_MODE_ISOLATED,
130
131
  } from './types/index.js'
@@ -162,6 +163,11 @@ import {
162
163
  fetchRegisteredApiKey,
163
164
  normalizeLighterPublicKey,
164
165
  } from './utils/registeredApiKey.js'
166
+ import {
167
+ collateralSpotBalance,
168
+ pooledSettlementSpendable,
169
+ pooledSettlementUnits,
170
+ } from './utils/settlementPool.js'
165
171
  import { spotPriceByAssetId, spotValuation } from './utils/spotPrice.js'
166
172
  import { isPlaceholderTxHash } from './utils/txHash.js'
167
173
  import { wireList } from './utils/wireList.js'
@@ -269,11 +275,9 @@ interface ReadOnlyCreationBackoff {
269
275
  * manager are created per plugin instance.
270
276
  *
271
277
  * Auth-token resolution order for the auth-gated reads:
272
- * 1. Per-call `options.lighterAuthToken`
273
- * 2. Constructor `authToken` (string or async factory)
274
- * 3. Persisted long-lived read-only token, keyed on the resolved Lighter
278
+ * 1. Persisted long-lived read-only token, keyed on the resolved Lighter
275
279
  * `accountIndex`
276
- * 4. Fresh 1h create via this instance's WASM signer + the user's registered
280
+ * 2. Fresh 1h create via this instance's WASM signer + the user's registered
277
281
  * API key
278
282
  *
279
283
  * Status reads report the account's state and do not throw for a missing
@@ -287,9 +291,9 @@ interface ReadOnlyCreationBackoff {
287
291
  * - `SetupRequired` when none of these sources yields a token. The SDK
288
292
  * sends no venue request. `getOrders`, `getOrder`, `getFills`,
289
293
  * `getPortfolioHistory` and `getActivity` need a token.
290
- * - `Unauthorized` when Lighter rejects the token that was sent. An
291
- * SDK-owned read-only token that Lighter reports as revoked is replaced
292
- * and the read retried once first.
294
+ * - `Unauthorized` when Lighter rejects the token that was sent. A
295
+ * read-only token that Lighter reports as revoked is replaced and the
296
+ * read retried once first.
293
297
  * - `AccountNotFound` when the wallet has no Lighter account. This also
294
298
  * applies to `getPositions`, `getWithdrawableBalances` and
295
299
  * `getAvailableToTrade`, which need no token. `getAvailableToTrade`
@@ -312,8 +316,6 @@ export interface LighterProviderOptions {
312
316
  * instance's signer and read-only token manager follow this URL.
313
317
  */
314
318
  restUrl?: string
315
- /** Pre-created Lighter read-only bearer, bypassing SDK token resolution. */
316
- authToken?: string | (() => string | Promise<string>)
317
319
  /** Token lifetime for on-demand standard-token creates (Lighter caps at 8h). Default 1h. */
318
320
  tokenLifetimeSeconds?: number
319
321
  /** Re-create when the cached standard token's remaining life is below this. Default 60s. */
@@ -344,8 +346,8 @@ interface CachedStandardToken {
344
346
  export interface LighterPerpsProvider extends PerpsProviderPlugin {
345
347
  /**
346
348
  * Resolve a Lighter auth token for `address`, following the resolution order
347
- * documented on {@link LighterProviderOptions} (the per-call override does not
348
- * apply here). Returns `undefined` when no source can produce a token.
349
+ * documented on {@link LighterProviderOptions}. Returns `undefined` when no
350
+ * source can produce a token.
349
351
  */
350
352
  resolveAuthToken(address: Address): Promise<string | undefined>
351
353
  }
@@ -381,12 +383,6 @@ export const createLighterProvider = (
381
383
  const explorerTxBaseUrl = deployment.explorerTxBaseUrl
382
384
  const bridgeChainId = deployment.bridgeChainId
383
385
  const collateral = deployment.collateral
384
- const authTokenSource: (() => string | Promise<string>) | undefined =
385
- typeof options.authToken === 'function'
386
- ? options.authToken
387
- : options.authToken !== undefined
388
- ? () => options.authToken as string
389
- : undefined
390
386
  const storage = options.storage ?? localStorageAdapter
391
387
  const signer = new LighterSigner({
392
388
  apiUrl: restUrl,
@@ -460,32 +456,20 @@ export const createLighterProvider = (
460
456
 
461
457
  /**
462
458
  * Resolve the bearer token for auth-gated reads. Priority:
463
- * 1. Per-call override (`options.lighterAuthToken`).
464
- * 2. Constructor-supplied token source.
465
- * 3. A stored read-only token — preferred for reads: it is read-only, so
459
+ * 1. A stored read-only token — preferred for reads: it is read-only, so
466
460
  * forwarding it (incl. to the backend) cannot authorise writes.
467
- * 4. A read-only token created on first use (via signer + registered API
461
+ * 2. A read-only token created on first use (via signer + registered API
468
462
  * key) and persisted for reuse.
469
- * 5. A standard auth token (read+write, 8h max) as a last resort — used
463
+ * 3. A standard auth token (read+write, 8h max) as a last resort — used
470
464
  * as the credential that authorises read-only token creation, and as
471
465
  * the fallback while creation is failing, bounded by the
472
466
  * creation-retry backoff.
473
467
  * Returns `undefined` when no source can produce a token.
474
468
  */
475
469
  const resolveAuthToken = async (
476
- opts: SDKRequestOptions | undefined,
477
- address?: Address,
470
+ address: Address,
478
471
  knownApiKey?: LighterApiKey
479
472
  ): Promise<string | undefined> => {
480
- if (opts?.lighterAuthToken !== undefined) {
481
- return opts.lighterAuthToken
482
- }
483
- if (authTokenSource !== undefined) {
484
- return authTokenSource()
485
- }
486
- if (address === undefined) {
487
- return undefined
488
- }
489
473
  const apiKey = knownApiKey ?? (await keyStore.get(address))
490
474
  if (apiKey === null) {
491
475
  return undefined
@@ -567,17 +551,15 @@ export const createLighterProvider = (
567
551
  /** {@link resolveAuthToken}, throwing `SetupRequired` when no source yields a token. */
568
552
  const requireAuthToken = async (
569
553
  read: string,
570
- opts: SDKRequestOptions | undefined,
571
554
  address: Address,
572
555
  knownApiKey?: LighterApiKey
573
556
  ): Promise<string> => {
574
- const token = await resolveAuthToken(opts, address, knownApiKey)
557
+ const token = await resolveAuthToken(address, knownApiKey)
575
558
  if (token === undefined) {
576
559
  throw new PerpsError(
577
560
  PerpsErrorCode.SetupRequired,
578
- `Lighter ${read} requires an auth token. Pass \`authToken\` to ` +
579
- 'lighterProvider, register an API key + signer for on-demand creation, or ' +
580
- 'forward `options.lighterAuthToken` on the call.'
561
+ `Lighter ${read} requires an auth token. Register an API key + signer ` +
562
+ 'for on-demand creation.'
581
563
  )
582
564
  }
583
565
  return token
@@ -638,7 +620,6 @@ export const createLighterProvider = (
638
620
  }
639
621
 
640
622
  const retryOnRevoked = async <T>(
641
- opts: SDKRequestOptions | undefined,
642
623
  address: Address,
643
624
  token: string,
644
625
  run: (token: string) => Promise<T>
@@ -646,9 +627,7 @@ export const createLighterProvider = (
646
627
  try {
647
628
  return await run(token)
648
629
  } catch (err) {
649
- const sdkOwnsToken =
650
- opts?.lighterAuthToken === undefined && authTokenSource === undefined
651
- if (!(err instanceof LighterTokenRevokedError) || !sdkOwnsToken) {
630
+ if (!(err instanceof LighterTokenRevokedError)) {
652
631
  throw err
653
632
  }
654
633
  const fresh = await replaceRevokedReadOnlyToken(address, token)
@@ -879,6 +858,20 @@ export const createLighterProvider = (
879
858
  return setup.find((descriptor) => descriptor.type === action)
880
859
  }
881
860
 
861
+ /**
862
+ * Refuse a SEND_ASSET for a unified account. Its settlement asset is one pool
863
+ * across both routes, so a perps↔spot transfer has nothing to move.
864
+ */
865
+ const assertSendAssetMode = async (address: Address): Promise<void> => {
866
+ const account = await fetchDetailedAccount(apiClient(), address)
867
+ if (account.account_trading_mode === LT_ACCOUNT_TRADING_MODE_UNIFIED) {
868
+ throw new PerpsError(
869
+ PerpsErrorCode.PooledCategoryTransfer,
870
+ 'Lighter unified accounts pool the settlement asset across perps and spot; a transfer between them has nothing to move'
871
+ )
872
+ }
873
+ }
874
+
882
875
  /**
883
876
  * Refuse an order signature for a tier no ACCOUNT_TYPE option binds. An
884
877
  * unreadable descriptor or tier fails open: the venue itself
@@ -908,11 +901,11 @@ export const createLighterProvider = (
908
901
  }
909
902
  let userTierName: string | undefined
910
903
  try {
911
- const token = await resolveAuthToken(undefined, address, localKey)
904
+ const token = await resolveAuthToken(address, localKey)
912
905
  const limits =
913
906
  token === undefined
914
907
  ? undefined
915
- : await retryOnRevoked(undefined, address, token, (resolvedToken) =>
908
+ : await retryOnRevoked(address, token, (resolvedToken) =>
916
909
  fetchAccountLimits(
917
910
  apiClient(),
918
911
  localKey.accountIndex,
@@ -951,7 +944,7 @@ export const createLighterProvider = (
951
944
  },
952
945
 
953
946
  resolveAuthToken(address: Address): Promise<string | undefined> {
954
- return resolveAuthToken(undefined, address)
947
+ return resolveAuthToken(address)
955
948
  },
956
949
 
957
950
  async getAccount(
@@ -981,7 +974,7 @@ export const createLighterProvider = (
981
974
  normalizeLighterPublicKey(registeredKey.public_key)
982
975
  // Status read: a missing or stale credential reports `apiKeyRegistered: false`, not a throw.
983
976
  const token = apiKeyRegistered
984
- ? await resolveAuthToken(opts, params.address, localKey)
977
+ ? await resolveAuthToken(params.address, localKey)
985
978
  : undefined
986
979
 
987
980
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -994,7 +987,7 @@ export const createLighterProvider = (
994
987
  getMarketsContext(requireClient(), { provider: providerKey }, opts),
995
988
  token === undefined
996
989
  ? Promise.resolve(undefined)
997
- : retryOnRevoked(opts, params.address, token, (resolvedToken) =>
990
+ : retryOnRevoked(params.address, token, (resolvedToken) =>
998
991
  fetchAccountLimits(client, account.index, resolvedToken)
999
992
  ),
1000
993
  apiKeyRegistered
@@ -1024,12 +1017,21 @@ export const createLighterProvider = (
1024
1017
  )
1025
1018
  // Each asset has a spot route (`balance`) and a perps route
1026
1019
  // (`margin_balance`). The instance's settlement asset is valued 1:1; an
1027
- // asset no spot market prices keeps a zero USD value.
1020
+ // asset no spot market prices keeps a zero USD value. A unified account
1021
+ // pools both settlement routes into one spot row.
1022
+ const isPooled = (asset: LtAccountAsset): boolean =>
1023
+ account.account_trading_mode === LT_ACCOUNT_TRADING_MODE_UNIFIED &&
1024
+ asset.asset_id === collateral.assetIndex
1028
1025
  const heldAssets = account.assets.filter((asset) =>
1029
- toRequiredBig(asset.balance, 'balance').gt(0)
1026
+ (isPooled(asset)
1027
+ ? pooledSettlementUnits(asset)
1028
+ : toRequiredBig(asset.balance, 'balance')
1029
+ ).gt(0)
1030
1030
  )
1031
- const marginAssets = account.assets.filter((asset) =>
1032
- toRequiredBig(asset.margin_balance, 'margin_balance').gt(0)
1031
+ const marginAssets = account.assets.filter(
1032
+ (asset) =>
1033
+ !isPooled(asset) &&
1034
+ toRequiredBig(asset.margin_balance, 'margin_balance').gt(0)
1033
1035
  )
1034
1036
  const spotPrices = spotPriceByAssetId(
1035
1037
  registry.markets,
@@ -1094,6 +1096,16 @@ export const createLighterProvider = (
1094
1096
  }
1095
1097
  })
1096
1098
  const balances: Balance[] = heldAssets.map((a) => {
1099
+ if (isPooled(a)) {
1100
+ const units = pooledSettlementUnits(a).toFixed()
1101
+ return {
1102
+ ...toBalance(a, LIGHTER_SPOT_CATEGORY_ID, registryAsset(a), units),
1103
+ transferable: calculateTransferable(
1104
+ pooledSettlementSpendable(a, availableBalance).toFixed(),
1105
+ units
1106
+ ),
1107
+ }
1108
+ }
1097
1109
  const balance = toRequiredBig(a.balance, 'balance')
1098
1110
  return {
1099
1111
  ...toBalance(
@@ -1132,6 +1144,10 @@ export const createLighterProvider = (
1132
1144
  crossAssetValue: account.cross_asset_value,
1133
1145
  crossInitialMarginRequirement: account.cross_initial_margin_requirement,
1134
1146
  totalAssetValue: account.total_asset_value,
1147
+ collateralSpotBalance: collateralSpotBalance(
1148
+ account.assets,
1149
+ collateral.assetIndex
1150
+ ),
1135
1151
  userTierName: limitsResult?.user_tier_name,
1136
1152
  accountTradingMode:
1137
1153
  account.account_trading_mode ?? LT_ACCOUNT_TRADING_MODE_SIMPLE,
@@ -1305,7 +1321,7 @@ export const createLighterProvider = (
1305
1321
  if (statuses.size === 0) {
1306
1322
  return empty
1307
1323
  }
1308
- const token = await requireAuthToken('orders read', opts, params.address)
1324
+ const token = await requireAuthToken('orders read', params.address)
1309
1325
 
1310
1326
  const client = apiClient(opts)
1311
1327
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -1327,7 +1343,6 @@ export const createLighterProvider = (
1327
1343
  }
1328
1344
  }
1329
1345
  const [activeResponse, inactiveResponse] = await retryOnRevoked(
1330
- opts,
1331
1346
  params.address,
1332
1347
  token,
1333
1348
  (t) =>
@@ -1394,7 +1409,7 @@ export const createLighterProvider = (
1394
1409
  params: ProviderGetOrderParams,
1395
1410
  opts?: SDKRequestOptions
1396
1411
  ): Promise<Order> {
1397
- const token = await requireAuthToken('order lookup', opts, params.address)
1412
+ const token = await requireAuthToken('order lookup', params.address)
1398
1413
 
1399
1414
  const client = apiClient(opts)
1400
1415
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -1437,7 +1452,7 @@ export const createLighterProvider = (
1437
1452
  `"${clientOrderIndex}" is not a decimal integer.`
1438
1453
  )
1439
1454
  }
1440
- const orders = await retryOnRevoked(opts, params.address, token, (t) =>
1455
+ const orders = await retryOnRevoked(params.address, token, (t) =>
1441
1456
  fetchOrdersByClientOrderIndex(
1442
1457
  client,
1443
1458
  t,
@@ -1461,7 +1476,7 @@ export const createLighterProvider = (
1461
1476
  const byOrderIndex = (o: LtOrder): boolean =>
1462
1477
  String(o.order_index) === params.id
1463
1478
 
1464
- const active = await retryOnRevoked(opts, params.address, token, (t) =>
1479
+ const active = await retryOnRevoked(params.address, token, (t) =>
1465
1480
  fetchActiveOrders(
1466
1481
  client,
1467
1482
  t,
@@ -1474,7 +1489,7 @@ export const createLighterProvider = (
1474
1489
  return detail(activeHit)
1475
1490
  }
1476
1491
 
1477
- const inactive = await retryOnRevoked(opts, params.address, token, (t) =>
1492
+ const inactive = await retryOnRevoked(params.address, token, (t) =>
1478
1493
  client.getAuthed<LtOrdersResponse>('/api/v1/accountInactiveOrders', t, {
1479
1494
  account_index: account.index,
1480
1495
  market_id: LIGHTER_ALL_MARKETS_WILDCARD,
@@ -1492,7 +1507,7 @@ export const createLighterProvider = (
1492
1507
  params: ProviderGetFillsParams,
1493
1508
  opts?: SDKRequestOptions
1494
1509
  ): Promise<FillsResponse> {
1495
- const token = await requireAuthToken('fills read', opts, params.address)
1510
+ const token = await requireAuthToken('fills read', params.address)
1496
1511
 
1497
1512
  const client = apiClient(opts)
1498
1513
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -1511,12 +1526,8 @@ export const createLighterProvider = (
1511
1526
  queryParams.cursor = params.cursor
1512
1527
  }
1513
1528
 
1514
- const response = await retryOnRevoked(
1515
- opts,
1516
- params.address,
1517
- token,
1518
- (tok) =>
1519
- client.getAuthed<LtTradesResponse>('/api/v1/trades', tok, queryParams)
1529
+ const response = await retryOnRevoked(params.address, token, (tok) =>
1530
+ client.getAuthed<LtTradesResponse>('/api/v1/trades', tok, queryParams)
1520
1531
  )
1521
1532
 
1522
1533
  // `get`, not `require`: a market id the backend list no longer carries
@@ -1546,12 +1557,9 @@ export const createLighterProvider = (
1546
1557
  params: ProviderGetPortfolioHistoryParams,
1547
1558
  opts?: SDKRequestOptions
1548
1559
  ): Promise<PortfolioHistoryResponse> {
1549
- const sdkOwnsToken =
1550
- opts?.lighterAuthToken === undefined && authTokenSource === undefined
1551
- const apiKey = sdkOwnsToken ? await keyStore.get(params.address) : null
1560
+ const apiKey = await keyStore.get(params.address)
1552
1561
  const token = await requireAuthToken(
1553
1562
  'portfolio history read',
1554
- opts,
1555
1563
  params.address,
1556
1564
  apiKey ?? undefined
1557
1565
  )
@@ -1580,23 +1588,24 @@ export const createLighterProvider = (
1580
1588
  // fallback sits outside `retryOnRevoked`, so a revocation the standard
1581
1589
  // token reports never replaces the read-only one. The key is re-read at
1582
1590
  // retry time because `REGISTER_API_KEY` can rotate it.
1583
- const response = await retryOnRevoked(
1584
- opts,
1585
- params.address,
1586
- token,
1587
- read
1588
- ).catch(async (err: unknown) => {
1589
- if (!(err instanceof LighterAuthRejectedError) || apiKey === null) {
1590
- throw err
1591
+ const response = await retryOnRevoked(params.address, token, read).catch(
1592
+ async (err: unknown) => {
1593
+ if (!(err instanceof LighterAuthRejectedError) || apiKey === null) {
1594
+ throw err
1595
+ }
1596
+ const current = (await keyStore.get(params.address)) ?? apiKey
1597
+ return read(
1598
+ await getStandardAuthToken(
1599
+ params.address,
1600
+ current.apiKeyPrivateKey,
1601
+ {
1602
+ apiKeyIndex: current.apiKeyIndex,
1603
+ accountIndex: current.accountIndex,
1604
+ }
1605
+ )
1606
+ )
1591
1607
  }
1592
- const current = (await keyStore.get(params.address)) ?? apiKey
1593
- return read(
1594
- await getStandardAuthToken(params.address, current.apiKeyPrivateKey, {
1595
- apiKeyIndex: current.apiKeyIndex,
1596
- accountIndex: current.accountIndex,
1597
- })
1598
- )
1599
- })
1608
+ )
1600
1609
 
1601
1610
  return mapPortfolioHistory(
1602
1611
  params.range,
@@ -1609,11 +1618,7 @@ export const createLighterProvider = (
1609
1618
  params: ProviderGetActivityParams,
1610
1619
  opts?: SDKRequestOptions
1611
1620
  ): Promise<ActivitiesResponse> {
1612
- const token = await requireAuthToken(
1613
- 'activity read',
1614
- opts,
1615
- params.address
1616
- )
1621
+ const token = await requireAuthToken('activity read', params.address)
1617
1622
 
1618
1623
  const inputCursor = decodeActivityCursor(params.cursor)
1619
1624
  const client = apiClient(opts)
@@ -1623,7 +1628,7 @@ export const createLighterProvider = (
1623
1628
  // Markets identify funding and liquidation rows; assets identify ledger
1624
1629
  // rows. A request for one surface must not pull the other's registry.
1625
1630
  const [history] = await Promise.all([
1626
- retryOnRevoked(opts, params.address, token, (t) =>
1631
+ retryOnRevoked(params.address, token, (t) =>
1627
1632
  fetchAllHistory(
1628
1633
  client,
1629
1634
  t,
@@ -1875,6 +1880,9 @@ export const createLighterProvider = (
1875
1880
  if (steps.some((step) => ORDER_PLACEMENT_ACTIONS.has(step.action))) {
1876
1881
  await assertOrderTier(address)
1877
1882
  }
1883
+ if (steps.some((step) => step.action === ActionType.SEND_ASSET)) {
1884
+ await assertSendAssetMode(address)
1885
+ }
1878
1886
  return lighterSignActions(
1879
1887
  {
1880
1888
  signer,
@@ -50,7 +50,8 @@ export const lighterPortfolioValue = (
50
50
 
51
51
  /**
52
52
  * Roll up settlement equity, spot holdings and non-settlement margin holdings.
53
- * Settlement margin is already included in `totalAssetValue`.
53
+ * The settlement asset counts as `totalAssetValue` plus its spot-route balance,
54
+ * so a row that pools both routes is not counted again.
54
55
  *
55
56
  * @throws {PerpsError} `SDKError` when the account is not a Lighter one.
56
57
  * @public
@@ -61,14 +62,11 @@ export function getAccountSummary(
61
62
  ): AccountSummary {
62
63
  const config = lighterConfig(account)
63
64
  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
+ const holdings = [...account.balances, ...account.collateralBalances].filter(
66
+ ({ asset }) =>
67
+ asset.id !== String(settlement.assetIndex) &&
68
+ asset.id !== settlement.displaySymbol
69
+ )
72
70
 
73
71
  let marginUsed = new Big(0)
74
72
  let unrealizedPnl = new Big(0)
@@ -79,7 +77,9 @@ export function getAccountSummary(
79
77
 
80
78
  return {
81
79
  portfolioValue: lighterPortfolioValue(
82
- toRequiredBig(config.totalAssetValue, 'totalAssetValue'),
80
+ toRequiredBig(config.totalAssetValue, 'totalAssetValue').plus(
81
+ toRequiredBig(config.collateralSpotBalance, 'collateralSpotBalance')
82
+ ),
83
83
  holdings.map((balance) => balance.valueUsd)
84
84
  ).toFixed(),
85
85
  availableMargin: atLeastZero(
@@ -33,8 +33,7 @@ export interface CreateAuthTokenInputs {
33
33
  * endpoints (`getOrders`, `getOrder`, `getActivity`, etc.).
34
34
  *
35
35
  * This utility carries no SDK-wide coupling — pass an explicit `signer` +
36
- * `apiKey` and consume the returned bearer however the caller likes
37
- * (per-call `options.lighterAuthToken`, persisted cache, etc.).
36
+ * `apiKey` and consume the returned bearer however the caller likes.
38
37
  * @public
39
38
  */
40
39
  export async function createAuthToken(
@@ -0,0 +1,36 @@
1
+ import type Big from 'big.js'
2
+ import type { LtAccountAsset } from '../types/account.js'
3
+ import { toRequiredBig } from './decimal.js'
4
+
5
+ /** Both routes of a unified account's settlement asset, as one holding. */
6
+ export const pooledSettlementUnits = (asset: LtAccountAsset): Big =>
7
+ toRequiredBig(asset.balance, 'balance').plus(
8
+ toRequiredBig(asset.margin_balance, 'margin_balance')
9
+ )
10
+
11
+ /**
12
+ * Spot units a unified account can spend from its settlement pool: the free
13
+ * spot route plus the margin route up to the account's `available_balance`.
14
+ * This is `computeSpotAvailableToTradeMarket` in Lighter's `lighter-ts` for an
15
+ * asset priced 1:1 with a `loan_to_value` of 1. The result can be negative.
16
+ */
17
+ export const pooledSettlementSpendable = (
18
+ asset: LtAccountAsset,
19
+ availableBalance: Big
20
+ ): Big => {
21
+ const marginBalance = toRequiredBig(asset.margin_balance, 'margin_balance')
22
+ return toRequiredBig(asset.balance, 'balance')
23
+ .minus(toRequiredBig(asset.locked_balance, 'locked_balance'))
24
+ .plus(marginBalance.lt(availableBalance) ? marginBalance : availableBalance)
25
+ }
26
+
27
+ /** Spot-route `balance` of the collateral asset; `'0'` when the account holds none. */
28
+ export const collateralSpotBalance = (
29
+ assets: readonly LtAccountAsset[],
30
+ settlementAssetIndex: number
31
+ ): string => {
32
+ const asset = assets.find((a) => a.asset_id === settlementAssetIndex)
33
+ return asset === undefined
34
+ ? '0'
35
+ : toRequiredBig(asset.balance, 'balance').toFixed()
36
+ }