@lifi/perps-sdk-provider-lighter 33.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 (67) 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 +53 -47
  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/liquidation.js.map +1 -1
  11. package/dist/cjs/utils/mapFill.js +1 -1
  12. package/dist/cjs/utils/mapFill.js.map +1 -1
  13. package/dist/cjs/utils/mapOrder.js +1 -1
  14. package/dist/cjs/utils/mapOrder.js.map +1 -1
  15. package/dist/cjs/utils/settlementPool.d.ts +6 -0
  16. package/dist/cjs/utils/settlementPool.d.ts.map +1 -0
  17. package/dist/cjs/utils/settlementPool.js +21 -0
  18. package/dist/cjs/utils/settlementPool.js.map +1 -0
  19. package/dist/cjs/utils/transferMargin.d.ts +2 -2
  20. package/dist/cjs/utils/transferMargin.d.ts.map +1 -1
  21. package/dist/cjs/utils/transferMargin.js +2 -2
  22. package/dist/cjs/utils/transferMargin.js.map +1 -1
  23. package/dist/esm/LighterProvider.d.ts +7 -11
  24. package/dist/esm/LighterProvider.d.ts.map +1 -1
  25. package/dist/esm/LighterProvider.js +63 -54
  26. package/dist/esm/LighterProvider.js.map +1 -1
  27. package/dist/esm/accountSummary.d.ts +2 -1
  28. package/dist/esm/accountSummary.d.ts.map +1 -1
  29. package/dist/esm/accountSummary.js +5 -7
  30. package/dist/esm/accountSummary.js.map +1 -1
  31. package/dist/esm/signers/createAuthToken.d.ts +1 -2
  32. package/dist/esm/signers/createAuthToken.d.ts.map +1 -1
  33. package/dist/esm/signers/createAuthToken.js +1 -2
  34. package/dist/esm/signers/createAuthToken.js.map +1 -1
  35. package/dist/esm/utils/liquidation.js +2 -2
  36. package/dist/esm/utils/liquidation.js.map +1 -1
  37. package/dist/esm/utils/mapFill.js +1 -1
  38. package/dist/esm/utils/mapFill.js.map +1 -1
  39. package/dist/esm/utils/mapOrder.js +1 -1
  40. package/dist/esm/utils/mapOrder.js.map +1 -1
  41. package/dist/esm/utils/settlementPool.d.ts +14 -0
  42. package/dist/esm/utils/settlementPool.d.ts.map +1 -0
  43. package/dist/esm/utils/settlementPool.js +23 -0
  44. package/dist/esm/utils/settlementPool.js.map +1 -0
  45. package/dist/esm/utils/transferMargin.d.ts +2 -2
  46. package/dist/esm/utils/transferMargin.d.ts.map +1 -1
  47. package/dist/esm/utils/transferMargin.js +2 -2
  48. package/dist/esm/utils/transferMargin.js.map +1 -1
  49. package/dist/types/LighterProvider.d.ts +7 -11
  50. package/dist/types/LighterProvider.d.ts.map +1 -1
  51. package/dist/types/accountSummary.d.ts +2 -1
  52. package/dist/types/accountSummary.d.ts.map +1 -1
  53. package/dist/types/signers/createAuthToken.d.ts +1 -2
  54. package/dist/types/signers/createAuthToken.d.ts.map +1 -1
  55. package/dist/types/utils/settlementPool.d.ts +14 -0
  56. package/dist/types/utils/settlementPool.d.ts.map +1 -0
  57. package/dist/types/utils/transferMargin.d.ts +2 -2
  58. package/dist/types/utils/transferMargin.d.ts.map +1 -1
  59. package/package.json +3 -3
  60. package/src/LighterProvider.ts +101 -92
  61. package/src/accountSummary.ts +10 -10
  62. package/src/signers/createAuthToken.ts +1 -2
  63. package/src/utils/liquidation.ts +2 -2
  64. package/src/utils/mapFill.ts +1 -1
  65. package/src/utils/mapOrder.ts +1 -1
  66. package/src/utils/settlementPool.ts +36 -0
  67. package/src/utils/transferMargin.ts +7 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settlementPool.d.ts","sourceRoot":"","sources":["../../../src/utils/settlementPool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,QAAQ,CAAA;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGzD,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,GAAI,OAAO,cAAc,KAAG,GAG3D,CAAA;AAEH;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GACpC,OAAO,cAAc,EACrB,kBAAkB,GAAG,KACpB,GAKF,CAAA;AAED,uFAAuF;AACvF,eAAO,MAAM,qBAAqB,GAChC,QAAQ,SAAS,cAAc,EAAE,EACjC,sBAAsB,MAAM,KAC3B,MAKF,CAAA"}
@@ -1,4 +1,4 @@
1
- import { type Position } from '@lifi/perps-types';
1
+ import { type DecimalString, type Position } from '@lifi/perps-types';
2
2
  /**
3
3
  * Lighter removable isolated margin:
4
4
  * `allocated_margin + unrealized PnL − initial margin requirement`. Lighter
@@ -13,5 +13,5 @@ import { type Position } from '@lifi/perps-types';
13
13
  * @see https://docs.lighter.xyz/trading/liquidations-and-llp-insurance-fund
14
14
  * @public
15
15
  */
16
- export declare function positionRemovableMargin(position: Position): string | undefined;
16
+ export declare function positionRemovableMargin(position: Position): DecimalString | undefined;
17
17
  //# sourceMappingURL=transferMargin.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"transferMargin.d.ts","sourceRoot":"","sources":["../../../src/utils/transferMargin.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAA;AA2B1B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,GACjB,aAAa,GAAG,SAAS,CAmB3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/perps-sdk-provider-lighter",
3
- "version": "33.0.0",
3
+ "version": "35.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": "^19.0.0"
47
+ "@lifi/perps-types": "^20.1.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@lifi/perps-sdk": "^21.1.0"
50
+ "@lifi/perps-sdk": "^23.0.0"
51
51
  },
52
52
  "publishConfig": {
53
53
  "access": "public"
@@ -9,7 +9,9 @@ import {
9
9
  getMarketsContext,
10
10
  getProviders,
11
11
  isActiveOrderStatus,
12
+ isDecimalString,
12
13
  localStorageAdapter,
14
+ numberToDecimalString,
13
15
  PerpsError,
14
16
  type PerpsProviderPlugin,
15
17
  type PerpsSDKClient,
@@ -65,7 +67,6 @@ import type {
65
67
  import {
66
68
  ActionType,
67
69
  ActivityType,
68
- isDecimalString,
69
70
  MarginMode,
70
71
  PerpsErrorCode,
71
72
  } from '@lifi/perps-types'
@@ -124,6 +125,7 @@ import type {
124
125
  } from './types/index.js'
125
126
  import {
126
127
  LT_ACCOUNT_TRADING_MODE_SIMPLE,
128
+ LT_ACCOUNT_TRADING_MODE_UNIFIED,
127
129
  LT_MARGIN_MODE_CROSS,
128
130
  LT_MARGIN_MODE_ISOLATED,
129
131
  } from './types/index.js'
@@ -161,6 +163,11 @@ import {
161
163
  fetchRegisteredApiKey,
162
164
  normalizeLighterPublicKey,
163
165
  } from './utils/registeredApiKey.js'
166
+ import {
167
+ collateralSpotBalance,
168
+ pooledSettlementSpendable,
169
+ pooledSettlementUnits,
170
+ } from './utils/settlementPool.js'
164
171
  import { spotPriceByAssetId, spotValuation } from './utils/spotPrice.js'
165
172
  import { isPlaceholderTxHash } from './utils/txHash.js'
166
173
  import { wireList } from './utils/wireList.js'
@@ -168,7 +175,7 @@ import { wireList } from './utils/wireList.js'
168
175
  const ZERO_FEE_TIER = { maker: '0', taker: '0' }
169
176
 
170
177
  const tickToFeeString = (tick: number): string =>
171
- String(tick / LIGHTER_FEE_TICK_SCALE)
178
+ numberToDecimalString(tick / LIGHTER_FEE_TICK_SCALE)
172
179
 
173
180
  const projectFeeTier = (
174
181
  limits: LtAccountLimits
@@ -268,11 +275,9 @@ interface ReadOnlyCreationBackoff {
268
275
  * manager are created per plugin instance.
269
276
  *
270
277
  * Auth-token resolution order for the auth-gated reads:
271
- * 1. Per-call `options.lighterAuthToken`
272
- * 2. Constructor `authToken` (string or async factory)
273
- * 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
274
279
  * `accountIndex`
275
- * 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
276
281
  * API key
277
282
  *
278
283
  * Status reads report the account's state and do not throw for a missing
@@ -286,9 +291,9 @@ interface ReadOnlyCreationBackoff {
286
291
  * - `SetupRequired` when none of these sources yields a token. The SDK
287
292
  * sends no venue request. `getOrders`, `getOrder`, `getFills`,
288
293
  * `getPortfolioHistory` and `getActivity` need a token.
289
- * - `Unauthorized` when Lighter rejects the token that was sent. An
290
- * SDK-owned read-only token that Lighter reports as revoked is replaced
291
- * 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.
292
297
  * - `AccountNotFound` when the wallet has no Lighter account. This also
293
298
  * applies to `getPositions`, `getWithdrawableBalances` and
294
299
  * `getAvailableToTrade`, which need no token. `getAvailableToTrade`
@@ -311,8 +316,6 @@ export interface LighterProviderOptions {
311
316
  * instance's signer and read-only token manager follow this URL.
312
317
  */
313
318
  restUrl?: string
314
- /** Pre-created Lighter read-only bearer, bypassing SDK token resolution. */
315
- authToken?: string | (() => string | Promise<string>)
316
319
  /** Token lifetime for on-demand standard-token creates (Lighter caps at 8h). Default 1h. */
317
320
  tokenLifetimeSeconds?: number
318
321
  /** Re-create when the cached standard token's remaining life is below this. Default 60s. */
@@ -343,8 +346,8 @@ interface CachedStandardToken {
343
346
  export interface LighterPerpsProvider extends PerpsProviderPlugin {
344
347
  /**
345
348
  * Resolve a Lighter auth token for `address`, following the resolution order
346
- * documented on {@link LighterProviderOptions} (the per-call override does not
347
- * 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.
348
351
  */
349
352
  resolveAuthToken(address: Address): Promise<string | undefined>
350
353
  }
@@ -380,12 +383,6 @@ export const createLighterProvider = (
380
383
  const explorerTxBaseUrl = deployment.explorerTxBaseUrl
381
384
  const bridgeChainId = deployment.bridgeChainId
382
385
  const collateral = deployment.collateral
383
- const authTokenSource: (() => string | Promise<string>) | undefined =
384
- typeof options.authToken === 'function'
385
- ? options.authToken
386
- : options.authToken !== undefined
387
- ? () => options.authToken as string
388
- : undefined
389
386
  const storage = options.storage ?? localStorageAdapter
390
387
  const signer = new LighterSigner({
391
388
  apiUrl: restUrl,
@@ -459,32 +456,20 @@ export const createLighterProvider = (
459
456
 
460
457
  /**
461
458
  * Resolve the bearer token for auth-gated reads. Priority:
462
- * 1. Per-call override (`options.lighterAuthToken`).
463
- * 2. Constructor-supplied token source.
464
- * 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
465
460
  * forwarding it (incl. to the backend) cannot authorise writes.
466
- * 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
467
462
  * key) and persisted for reuse.
468
- * 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
469
464
  * as the credential that authorises read-only token creation, and as
470
465
  * the fallback while creation is failing, bounded by the
471
466
  * creation-retry backoff.
472
467
  * Returns `undefined` when no source can produce a token.
473
468
  */
474
469
  const resolveAuthToken = async (
475
- opts: SDKRequestOptions | undefined,
476
- address?: Address,
470
+ address: Address,
477
471
  knownApiKey?: LighterApiKey
478
472
  ): Promise<string | undefined> => {
479
- if (opts?.lighterAuthToken !== undefined) {
480
- return opts.lighterAuthToken
481
- }
482
- if (authTokenSource !== undefined) {
483
- return authTokenSource()
484
- }
485
- if (address === undefined) {
486
- return undefined
487
- }
488
473
  const apiKey = knownApiKey ?? (await keyStore.get(address))
489
474
  if (apiKey === null) {
490
475
  return undefined
@@ -566,17 +551,15 @@ export const createLighterProvider = (
566
551
  /** {@link resolveAuthToken}, throwing `SetupRequired` when no source yields a token. */
567
552
  const requireAuthToken = async (
568
553
  read: string,
569
- opts: SDKRequestOptions | undefined,
570
554
  address: Address,
571
555
  knownApiKey?: LighterApiKey
572
556
  ): Promise<string> => {
573
- const token = await resolveAuthToken(opts, address, knownApiKey)
557
+ const token = await resolveAuthToken(address, knownApiKey)
574
558
  if (token === undefined) {
575
559
  throw new PerpsError(
576
560
  PerpsErrorCode.SetupRequired,
577
- `Lighter ${read} requires an auth token. Pass \`authToken\` to ` +
578
- 'lighterProvider, register an API key + signer for on-demand creation, or ' +
579
- 'forward `options.lighterAuthToken` on the call.'
561
+ `Lighter ${read} requires an auth token. Register an API key + signer ` +
562
+ 'for on-demand creation.'
580
563
  )
581
564
  }
582
565
  return token
@@ -637,7 +620,6 @@ export const createLighterProvider = (
637
620
  }
638
621
 
639
622
  const retryOnRevoked = async <T>(
640
- opts: SDKRequestOptions | undefined,
641
623
  address: Address,
642
624
  token: string,
643
625
  run: (token: string) => Promise<T>
@@ -645,9 +627,7 @@ export const createLighterProvider = (
645
627
  try {
646
628
  return await run(token)
647
629
  } catch (err) {
648
- const sdkOwnsToken =
649
- opts?.lighterAuthToken === undefined && authTokenSource === undefined
650
- if (!(err instanceof LighterTokenRevokedError) || !sdkOwnsToken) {
630
+ if (!(err instanceof LighterTokenRevokedError)) {
651
631
  throw err
652
632
  }
653
633
  const fresh = await replaceRevokedReadOnlyToken(address, token)
@@ -878,6 +858,20 @@ export const createLighterProvider = (
878
858
  return setup.find((descriptor) => descriptor.type === action)
879
859
  }
880
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
+
881
875
  /**
882
876
  * Refuse an order signature for a tier no ACCOUNT_TYPE option binds. An
883
877
  * unreadable descriptor or tier fails open: the venue itself
@@ -907,11 +901,11 @@ export const createLighterProvider = (
907
901
  }
908
902
  let userTierName: string | undefined
909
903
  try {
910
- const token = await resolveAuthToken(undefined, address, localKey)
904
+ const token = await resolveAuthToken(address, localKey)
911
905
  const limits =
912
906
  token === undefined
913
907
  ? undefined
914
- : await retryOnRevoked(undefined, address, token, (resolvedToken) =>
908
+ : await retryOnRevoked(address, token, (resolvedToken) =>
915
909
  fetchAccountLimits(
916
910
  apiClient(),
917
911
  localKey.accountIndex,
@@ -950,7 +944,7 @@ export const createLighterProvider = (
950
944
  },
951
945
 
952
946
  resolveAuthToken(address: Address): Promise<string | undefined> {
953
- return resolveAuthToken(undefined, address)
947
+ return resolveAuthToken(address)
954
948
  },
955
949
 
956
950
  async getAccount(
@@ -980,7 +974,7 @@ export const createLighterProvider = (
980
974
  normalizeLighterPublicKey(registeredKey.public_key)
981
975
  // Status read: a missing or stale credential reports `apiKeyRegistered: false`, not a throw.
982
976
  const token = apiKeyRegistered
983
- ? await resolveAuthToken(opts, params.address, localKey)
977
+ ? await resolveAuthToken(params.address, localKey)
984
978
  : undefined
985
979
 
986
980
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -993,7 +987,7 @@ export const createLighterProvider = (
993
987
  getMarketsContext(requireClient(), { provider: providerKey }, opts),
994
988
  token === undefined
995
989
  ? Promise.resolve(undefined)
996
- : retryOnRevoked(opts, params.address, token, (resolvedToken) =>
990
+ : retryOnRevoked(params.address, token, (resolvedToken) =>
997
991
  fetchAccountLimits(client, account.index, resolvedToken)
998
992
  ),
999
993
  apiKeyRegistered
@@ -1023,12 +1017,21 @@ export const createLighterProvider = (
1023
1017
  )
1024
1018
  // Each asset has a spot route (`balance`) and a perps route
1025
1019
  // (`margin_balance`). The instance's settlement asset is valued 1:1; an
1026
- // 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
1027
1025
  const heldAssets = account.assets.filter((asset) =>
1028
- toRequiredBig(asset.balance, 'balance').gt(0)
1026
+ (isPooled(asset)
1027
+ ? pooledSettlementUnits(asset)
1028
+ : toRequiredBig(asset.balance, 'balance')
1029
+ ).gt(0)
1029
1030
  )
1030
- const marginAssets = account.assets.filter((asset) =>
1031
- 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)
1032
1035
  )
1033
1036
  const spotPrices = spotPriceByAssetId(
1034
1037
  registry.markets,
@@ -1093,6 +1096,16 @@ export const createLighterProvider = (
1093
1096
  }
1094
1097
  })
1095
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
+ }
1096
1109
  const balance = toRequiredBig(a.balance, 'balance')
1097
1110
  return {
1098
1111
  ...toBalance(
@@ -1131,6 +1144,10 @@ export const createLighterProvider = (
1131
1144
  crossAssetValue: account.cross_asset_value,
1132
1145
  crossInitialMarginRequirement: account.cross_initial_margin_requirement,
1133
1146
  totalAssetValue: account.total_asset_value,
1147
+ collateralSpotBalance: collateralSpotBalance(
1148
+ account.assets,
1149
+ collateral.assetIndex
1150
+ ),
1134
1151
  userTierName: limitsResult?.user_tier_name,
1135
1152
  accountTradingMode:
1136
1153
  account.account_trading_mode ?? LT_ACCOUNT_TRADING_MODE_SIMPLE,
@@ -1304,7 +1321,7 @@ export const createLighterProvider = (
1304
1321
  if (statuses.size === 0) {
1305
1322
  return empty
1306
1323
  }
1307
- const token = await requireAuthToken('orders read', opts, params.address)
1324
+ const token = await requireAuthToken('orders read', params.address)
1308
1325
 
1309
1326
  const client = apiClient(opts)
1310
1327
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -1326,7 +1343,6 @@ export const createLighterProvider = (
1326
1343
  }
1327
1344
  }
1328
1345
  const [activeResponse, inactiveResponse] = await retryOnRevoked(
1329
- opts,
1330
1346
  params.address,
1331
1347
  token,
1332
1348
  (t) =>
@@ -1393,7 +1409,7 @@ export const createLighterProvider = (
1393
1409
  params: ProviderGetOrderParams,
1394
1410
  opts?: SDKRequestOptions
1395
1411
  ): Promise<Order> {
1396
- const token = await requireAuthToken('order lookup', opts, params.address)
1412
+ const token = await requireAuthToken('order lookup', params.address)
1397
1413
 
1398
1414
  const client = apiClient(opts)
1399
1415
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -1436,7 +1452,7 @@ export const createLighterProvider = (
1436
1452
  `"${clientOrderIndex}" is not a decimal integer.`
1437
1453
  )
1438
1454
  }
1439
- const orders = await retryOnRevoked(opts, params.address, token, (t) =>
1455
+ const orders = await retryOnRevoked(params.address, token, (t) =>
1440
1456
  fetchOrdersByClientOrderIndex(
1441
1457
  client,
1442
1458
  t,
@@ -1460,7 +1476,7 @@ export const createLighterProvider = (
1460
1476
  const byOrderIndex = (o: LtOrder): boolean =>
1461
1477
  String(o.order_index) === params.id
1462
1478
 
1463
- const active = await retryOnRevoked(opts, params.address, token, (t) =>
1479
+ const active = await retryOnRevoked(params.address, token, (t) =>
1464
1480
  fetchActiveOrders(
1465
1481
  client,
1466
1482
  t,
@@ -1473,7 +1489,7 @@ export const createLighterProvider = (
1473
1489
  return detail(activeHit)
1474
1490
  }
1475
1491
 
1476
- const inactive = await retryOnRevoked(opts, params.address, token, (t) =>
1492
+ const inactive = await retryOnRevoked(params.address, token, (t) =>
1477
1493
  client.getAuthed<LtOrdersResponse>('/api/v1/accountInactiveOrders', t, {
1478
1494
  account_index: account.index,
1479
1495
  market_id: LIGHTER_ALL_MARKETS_WILDCARD,
@@ -1491,7 +1507,7 @@ export const createLighterProvider = (
1491
1507
  params: ProviderGetFillsParams,
1492
1508
  opts?: SDKRequestOptions
1493
1509
  ): Promise<FillsResponse> {
1494
- const token = await requireAuthToken('fills read', opts, params.address)
1510
+ const token = await requireAuthToken('fills read', params.address)
1495
1511
 
1496
1512
  const client = apiClient(opts)
1497
1513
  const registry = getMarketRegistry(requireClient(), providerKey)
@@ -1510,12 +1526,8 @@ export const createLighterProvider = (
1510
1526
  queryParams.cursor = params.cursor
1511
1527
  }
1512
1528
 
1513
- const response = await retryOnRevoked(
1514
- opts,
1515
- params.address,
1516
- token,
1517
- (tok) =>
1518
- 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)
1519
1531
  )
1520
1532
 
1521
1533
  // `get`, not `require`: a market id the backend list no longer carries
@@ -1545,12 +1557,9 @@ export const createLighterProvider = (
1545
1557
  params: ProviderGetPortfolioHistoryParams,
1546
1558
  opts?: SDKRequestOptions
1547
1559
  ): Promise<PortfolioHistoryResponse> {
1548
- const sdkOwnsToken =
1549
- opts?.lighterAuthToken === undefined && authTokenSource === undefined
1550
- const apiKey = sdkOwnsToken ? await keyStore.get(params.address) : null
1560
+ const apiKey = await keyStore.get(params.address)
1551
1561
  const token = await requireAuthToken(
1552
1562
  'portfolio history read',
1553
- opts,
1554
1563
  params.address,
1555
1564
  apiKey ?? undefined
1556
1565
  )
@@ -1579,23 +1588,24 @@ export const createLighterProvider = (
1579
1588
  // fallback sits outside `retryOnRevoked`, so a revocation the standard
1580
1589
  // token reports never replaces the read-only one. The key is re-read at
1581
1590
  // retry time because `REGISTER_API_KEY` can rotate it.
1582
- const response = await retryOnRevoked(
1583
- opts,
1584
- params.address,
1585
- token,
1586
- read
1587
- ).catch(async (err: unknown) => {
1588
- if (!(err instanceof LighterAuthRejectedError) || apiKey === null) {
1589
- 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
+ )
1590
1607
  }
1591
- const current = (await keyStore.get(params.address)) ?? apiKey
1592
- return read(
1593
- await getStandardAuthToken(params.address, current.apiKeyPrivateKey, {
1594
- apiKeyIndex: current.apiKeyIndex,
1595
- accountIndex: current.accountIndex,
1596
- })
1597
- )
1598
- })
1608
+ )
1599
1609
 
1600
1610
  return mapPortfolioHistory(
1601
1611
  params.range,
@@ -1608,11 +1618,7 @@ export const createLighterProvider = (
1608
1618
  params: ProviderGetActivityParams,
1609
1619
  opts?: SDKRequestOptions
1610
1620
  ): Promise<ActivitiesResponse> {
1611
- const token = await requireAuthToken(
1612
- 'activity read',
1613
- opts,
1614
- params.address
1615
- )
1621
+ const token = await requireAuthToken('activity read', params.address)
1616
1622
 
1617
1623
  const inputCursor = decodeActivityCursor(params.cursor)
1618
1624
  const client = apiClient(opts)
@@ -1622,7 +1628,7 @@ export const createLighterProvider = (
1622
1628
  // Markets identify funding and liquidation rows; assets identify ledger
1623
1629
  // rows. A request for one surface must not pull the other's registry.
1624
1630
  const [history] = await Promise.all([
1625
- retryOnRevoked(opts, params.address, token, (t) =>
1631
+ retryOnRevoked(params.address, token, (t) =>
1626
1632
  fetchAllHistory(
1627
1633
  client,
1628
1634
  t,
@@ -1874,6 +1880,9 @@ export const createLighterProvider = (
1874
1880
  if (steps.some((step) => ORDER_PLACEMENT_ACTIONS.has(step.action))) {
1875
1881
  await assertOrderTier(address)
1876
1882
  }
1883
+ if (steps.some((step) => step.action === ActionType.SEND_ASSET)) {
1884
+ await assertSendAssetMode(address)
1885
+ }
1877
1886
  return lighterSignActions(
1878
1887
  {
1879
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(
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import {
11
- estimateLiquidationPrice as estimateIsolatedLiquidationPrice,
11
+ estimateLiquidationPrice as estimateLiquidationPriceAtRate,
12
12
  type LiquidationEstimateParams,
13
13
  } from '@lifi/perps-sdk'
14
14
  import type { PerpsMarket } from '@lifi/perps-types'
@@ -30,7 +30,7 @@ export function estimateLiquidationPrice(
30
30
  if (market.maintenanceMarginRate === undefined) {
31
31
  return undefined
32
32
  }
33
- return estimateIsolatedLiquidationPrice({
33
+ return estimateLiquidationPriceAtRate({
34
34
  entryPrice: params.entryPrice,
35
35
  leverage: params.leverage,
36
36
  isLong: params.isLong,
@@ -76,7 +76,7 @@ const deriveRealizedPnl = (
76
76
  const pnl = isLong
77
77
  ? price.minus(avgEntry).times(closedSize)
78
78
  : avgEntry.minus(price).times(closedSize)
79
- return pnl.eq(0) ? null : pnl.toString()
79
+ return pnl.eq(0) ? null : pnl.toFixed()
80
80
  }
81
81
 
82
82
  /**
@@ -118,7 +118,7 @@ export const mapOrder = (order: LtOrder, market: MarketDisplay): Order => {
118
118
  ? {
119
119
  averagePrice: new Big(order.filled_quote_amount)
120
120
  .div(filledSize)
121
- .toString(),
121
+ .toFixed(),
122
122
  }
123
123
  : {}),
124
124
  reduceOnly: order.reduce_only,
@@ -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
+ }
@@ -1,9 +1,12 @@
1
- import { PerpsError } from '@lifi/perps-sdk'
2
1
  import {
3
- PerpsErrorCode,
4
- type Position,
2
+ PerpsError,
5
3
  positionSupportsMarginAdjustment,
6
4
  positionSupportsMarginRemoval,
5
+ } from '@lifi/perps-sdk'
6
+ import {
7
+ type DecimalString,
8
+ PerpsErrorCode,
9
+ type Position,
7
10
  } from '@lifi/perps-types'
8
11
  import Big from 'big.js'
9
12
 
@@ -47,7 +50,7 @@ function positivePositionAmount(value: string, field: string): Big {
47
50
  */
48
51
  export function positionRemovableMargin(
49
52
  position: Position
50
- ): string | undefined {
53
+ ): DecimalString | undefined {
51
54
  if (!positionSupportsMarginAdjustment(position)) {
52
55
  return undefined
53
56
  }