@drift-labs/sdk 2.149.0-beta.0 → 2.149.0-beta.1

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.
package/src/user.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  UserAccount,
15
15
  UserStatus,
16
16
  UserStatsAccount,
17
+ AccountLiquidatableStatus,
17
18
  } from './types';
18
19
  import {
19
20
  calculateEntryPrice,
@@ -68,6 +69,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade';
68
69
  import {
69
70
  MarketType,
70
71
  PositionDirection,
72
+ PositionFlag,
71
73
  SpotBalanceType,
72
74
  SpotMarketAccount,
73
75
  } from './types';
@@ -106,6 +108,13 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice';
106
108
 
107
109
  import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel';
108
110
  import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber';
111
+ import {
112
+ IsolatedMarginCalculation,
113
+ MarginCalculation,
114
+ MarginContext,
115
+ } from './marginCalculation';
116
+
117
+ export type MarginType = 'Cross' | 'Isolated';
109
118
 
110
119
  export class User {
111
120
  driftClient: DriftClient;
@@ -343,6 +352,23 @@ export class User {
343
352
  };
344
353
  }
345
354
 
355
+ public getIsolatePerpPositionTokenAmount(perpMarketIndex: number): BN {
356
+ const perpPosition = this.getPerpPosition(perpMarketIndex);
357
+ if (!perpPosition) return ZERO;
358
+ const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex);
359
+ const spotMarket = this.driftClient.getSpotMarketAccount(
360
+ perpMarket.quoteSpotMarketIndex
361
+ );
362
+ if (perpPosition === undefined) {
363
+ return ZERO;
364
+ }
365
+ return getTokenAmount(
366
+ perpPosition.isolatedPositionScaledBalance ?? ZERO, //TODO remove ? later
367
+ spotMarket,
368
+ SpotBalanceType.DEPOSIT
369
+ );
370
+ }
371
+
346
372
  public getClonedPosition(position: PerpPosition): PerpPosition {
347
373
  const clonedPosition = Object.assign({}, position);
348
374
  return clonedPosition;
@@ -515,62 +541,130 @@ export class User {
515
541
  */
516
542
  public getFreeCollateral(
517
543
  marginCategory: MarginCategory = 'Initial',
518
- enterHighLeverageMode = undefined
544
+ enterHighLeverageMode = false,
545
+ perpMarketIndex?: number
519
546
  ): BN {
520
- const totalCollateral = this.getTotalCollateral(marginCategory, true);
521
- const marginRequirement =
522
- marginCategory === 'Initial'
523
- ? this.getInitialMarginRequirement(enterHighLeverageMode)
524
- : this.getMaintenanceMarginRequirement();
525
- const freeCollateral = totalCollateral.sub(marginRequirement);
526
- return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
547
+ const { totalCollateral, marginRequirement, getIsolatedFreeCollateral } =
548
+ this.getMarginCalculation(marginCategory, {
549
+ enteringHighLeverage: enterHighLeverageMode,
550
+ strict: marginCategory === 'Initial',
551
+ });
552
+
553
+ if (perpMarketIndex !== undefined) {
554
+ return getIsolatedFreeCollateral(perpMarketIndex);
555
+ } else {
556
+ const freeCollateral = totalCollateral.sub(marginRequirement);
557
+ return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
558
+ }
527
559
  }
528
560
 
529
561
  /**
530
- * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION
562
+ * @deprecated Use the overload that includes { marginType, perpMarketIndex }
531
563
  */
532
564
  public getMarginRequirement(
533
565
  marginCategory: MarginCategory,
534
566
  liquidationBuffer?: BN,
535
- strict = false,
536
- includeOpenOrders = true,
537
- enteringHighLeverage = undefined
567
+ strict?: boolean,
568
+ includeOpenOrders?: boolean,
569
+ enteringHighLeverage?: boolean
570
+ ): BN;
571
+
572
+ /**
573
+ * Calculates the margin requirement based on the specified parameters.
574
+ *
575
+ * @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance').
576
+ * @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios.
577
+ * @param strict - Optional flag to enforce strict margin calculations.
578
+ * @param includeOpenOrders - Optional flag to include open orders in the margin calculation.
579
+ * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode.
580
+ * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'.
581
+ *
582
+ * @returns The calculated margin requirement as a BN (BigNumber).
583
+ */
584
+ public getMarginRequirement(
585
+ marginCategory: MarginCategory,
586
+ liquidationBuffer?: BN,
587
+ strict?: boolean,
588
+ includeOpenOrders?: boolean,
589
+ enteringHighLeverage?: boolean,
590
+ perpMarketIndex?: number
591
+ ): BN;
592
+
593
+ public getMarginRequirement(
594
+ marginCategory: MarginCategory,
595
+ liquidationBuffer?: BN,
596
+ strict?: boolean,
597
+ includeOpenOrders?: boolean,
598
+ enteringHighLeverage?: boolean,
599
+ perpMarketIndex?: number
538
600
  ): BN {
539
- return this.getTotalPerpPositionLiability(
540
- marginCategory,
541
- liquidationBuffer,
542
- includeOpenOrders,
601
+ const liquidationBufferMap = new Map();
602
+ if (liquidationBuffer && perpMarketIndex !== undefined) {
603
+ liquidationBufferMap.set(perpMarketIndex, liquidationBuffer);
604
+ } else if (liquidationBuffer) {
605
+ liquidationBufferMap.set('cross', liquidationBuffer);
606
+ }
607
+
608
+ const marginCalc = this.getMarginCalculation(marginCategory, {
543
609
  strict,
544
- enteringHighLeverage
545
- ).add(
546
- this.getSpotMarketLiabilityValue(
547
- undefined,
548
- marginCategory,
549
- liquidationBuffer,
550
- includeOpenOrders,
551
- strict
552
- )
553
- );
610
+ includeOpenOrders,
611
+ enteringHighLeverage,
612
+ liquidationBufferMap,
613
+ });
614
+
615
+ // If perpMarketIndex is provided, compute only for that market index
616
+ if (perpMarketIndex !== undefined) {
617
+ const isolatedMarginCalculation =
618
+ marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
619
+ if (!isolatedMarginCalculation) return ZERO;
620
+ const { marginRequirement, marginRequirementPlusBuffer } =
621
+ isolatedMarginCalculation;
622
+
623
+ if (liquidationBuffer?.gt(ZERO)) {
624
+ return marginRequirementPlusBuffer;
625
+ }
626
+ return marginRequirement;
627
+ }
628
+
629
+ // Default: Cross margin requirement
630
+ if (liquidationBuffer?.gt(ZERO)) {
631
+ return marginCalc.marginRequirementPlusBuffer;
632
+ }
633
+ return marginCalc.marginRequirement;
554
634
  }
555
635
 
556
636
  /**
557
637
  * @returns The initial margin requirement in USDC. : QUOTE_PRECISION
558
638
  */
559
- public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN {
639
+ public getInitialMarginRequirement(
640
+ enterHighLeverageMode = false,
641
+ perpMarketIndex?: number
642
+ ): BN {
560
643
  return this.getMarginRequirement(
561
644
  'Initial',
562
645
  undefined,
563
646
  true,
564
647
  undefined,
565
- enterHighLeverageMode
648
+ enterHighLeverageMode,
649
+ perpMarketIndex
566
650
  );
567
651
  }
568
652
 
569
653
  /**
570
654
  * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION
571
655
  */
572
- public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN {
573
- return this.getMarginRequirement('Maintenance', liquidationBuffer);
656
+ public getMaintenanceMarginRequirement(
657
+ liquidationBuffer?: BN,
658
+ perpMarketIndex?: number
659
+ ): BN {
660
+ return this.getMarginRequirement(
661
+ 'Maintenance',
662
+ liquidationBuffer,
663
+ false, // strict default
664
+ true, // includeOpenOrders default
665
+ false, // enteringHighLeverage default
666
+ perpMarketIndex
667
+ );
574
668
  }
575
669
 
576
670
  public getActivePerpPositionsForUserAccount(
@@ -580,7 +674,8 @@ export class User {
580
674
  (pos) =>
581
675
  !pos.baseAssetAmount.eq(ZERO) ||
582
676
  !pos.quoteAssetAmount.eq(ZERO) ||
583
- !(pos.openOrders == 0)
677
+ !(pos.openOrders == 0) ||
678
+ pos.isolatedPositionScaledBalance?.gt(ZERO)
584
679
  );
585
680
  }
586
681
 
@@ -1153,46 +1248,93 @@ export class User {
1153
1248
  marginCategory: MarginCategory = 'Initial',
1154
1249
  strict = false,
1155
1250
  includeOpenOrders = true,
1156
- liquidationBuffer?: BN
1251
+ liquidationBuffer?: BN,
1252
+ perpMarketIndex?: number
1157
1253
  ): BN {
1158
- return this.getSpotMarketAssetValue(
1159
- undefined,
1160
- marginCategory,
1254
+ const liquidationBufferMap = (() => {
1255
+ if (liquidationBuffer && perpMarketIndex !== undefined) {
1256
+ return new Map([[perpMarketIndex, liquidationBuffer]]);
1257
+ } else if (liquidationBuffer) {
1258
+ return new Map([['cross', liquidationBuffer]]);
1259
+ }
1260
+ return new Map();
1261
+ })();
1262
+ const marginCalc = this.getMarginCalculation(marginCategory, {
1263
+ strict,
1161
1264
  includeOpenOrders,
1162
- strict
1163
- ).add(
1164
- this.getUnrealizedPNL(
1165
- true,
1166
- undefined,
1167
- marginCategory,
1168
- strict,
1169
- liquidationBuffer
1170
- )
1171
- );
1265
+ liquidationBufferMap,
1266
+ });
1267
+
1268
+ if (perpMarketIndex !== undefined) {
1269
+ const { totalCollateral, totalCollateralBuffer } =
1270
+ marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
1271
+ if (liquidationBuffer?.gt(ZERO)) {
1272
+ return totalCollateralBuffer;
1273
+ }
1274
+ return totalCollateral;
1275
+ }
1276
+
1277
+ if (liquidationBuffer?.gt(ZERO)) {
1278
+ return marginCalc.totalCollateralBuffer;
1279
+ }
1280
+ return marginCalc.totalCollateral;
1172
1281
  }
1173
1282
 
1174
- public getLiquidationBuffer(): BN | undefined {
1175
- // if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer
1176
- let liquidationBuffer = undefined;
1283
+ public getLiquidationBuffer(): Map<number | 'cross', BN> {
1284
+ const liquidationBufferMap = new Map<number | 'cross', BN>();
1177
1285
  if (this.isBeingLiquidated()) {
1178
- liquidationBuffer = new BN(
1179
- this.driftClient.getStateAccount().liquidationMarginBufferRatio
1286
+ liquidationBufferMap.set(
1287
+ 'cross',
1288
+ new BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio)
1180
1289
  );
1181
1290
  }
1182
- return liquidationBuffer;
1291
+ for (const position of this.getActivePerpPositions()) {
1292
+ if (
1293
+ position.positionFlag &
1294
+ (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)
1295
+ ) {
1296
+ liquidationBufferMap.set(
1297
+ position.marketIndex,
1298
+ new BN(
1299
+ this.driftClient.getStateAccount().liquidationMarginBufferRatio
1300
+ )
1301
+ );
1302
+ }
1303
+ }
1304
+ return liquidationBufferMap;
1183
1305
  }
1184
1306
 
1185
1307
  /**
1186
1308
  * calculates User Health by comparing total collateral and maint. margin requirement
1187
1309
  * @returns : number (value from [0, 100])
1188
1310
  */
1189
- public getHealth(): number {
1190
- if (this.isBeingLiquidated()) {
1311
+ public getHealth(perpMarketIndex?: number): number {
1312
+ if (this.isCrossMarginBeingLiquidated() && !perpMarketIndex) {
1313
+ return 0;
1314
+ }
1315
+ if (
1316
+ perpMarketIndex &&
1317
+ this.isIsolatedPositionBeingLiquidated(perpMarketIndex)
1318
+ ) {
1191
1319
  return 0;
1192
1320
  }
1193
1321
 
1194
- const totalCollateral = this.getTotalCollateral('Maintenance');
1195
- const maintenanceMarginReq = this.getMaintenanceMarginRequirement();
1322
+ const marginCalc = this.getMarginCalculation('Maintenance');
1323
+
1324
+ let totalCollateral: BN;
1325
+ let maintenanceMarginReq: BN;
1326
+
1327
+ if (perpMarketIndex) {
1328
+ const isolatedMarginCalc =
1329
+ marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
1330
+ if (isolatedMarginCalc) {
1331
+ totalCollateral = isolatedMarginCalc.totalCollateral;
1332
+ maintenanceMarginReq = isolatedMarginCalc.marginRequirement;
1333
+ }
1334
+ } else {
1335
+ totalCollateral = marginCalc.totalCollateral;
1336
+ maintenanceMarginReq = marginCalc.marginRequirement;
1337
+ }
1196
1338
 
1197
1339
  let health: number;
1198
1340
 
@@ -1491,9 +1633,9 @@ export class User {
1491
1633
  * calculates current user leverage which is (total liability size) / (net asset value)
1492
1634
  * @returns : Precision TEN_THOUSAND
1493
1635
  */
1494
- public getLeverage(includeOpenOrders = true): BN {
1636
+ public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN {
1495
1637
  return this.calculateLeverageFromComponents(
1496
- this.getLeverageComponents(includeOpenOrders)
1638
+ this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex)
1497
1639
  );
1498
1640
  }
1499
1641
 
@@ -1521,13 +1663,67 @@ export class User {
1521
1663
 
1522
1664
  getLeverageComponents(
1523
1665
  includeOpenOrders = true,
1524
- marginCategory: MarginCategory = undefined
1666
+ marginCategory: MarginCategory = undefined,
1667
+ perpMarketIndex?: number
1525
1668
  ): {
1526
1669
  perpLiabilityValue: BN;
1527
1670
  perpPnl: BN;
1528
1671
  spotAssetValue: BN;
1529
1672
  spotLiabilityValue: BN;
1530
1673
  } {
1674
+ if (perpMarketIndex) {
1675
+ const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex);
1676
+ const perpLiability = this.calculateWeightedPerpPositionLiability(
1677
+ perpPosition,
1678
+ marginCategory,
1679
+ undefined,
1680
+ includeOpenOrders
1681
+ );
1682
+ const perpMarket = this.driftClient.getPerpMarketAccount(
1683
+ perpPosition.marketIndex
1684
+ );
1685
+
1686
+ const oraclePriceData = this.getOracleDataForPerpMarket(
1687
+ perpPosition.marketIndex
1688
+ );
1689
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
1690
+ perpMarket.quoteSpotMarketIndex
1691
+ );
1692
+ const quoteOraclePriceData = this.getOracleDataForSpotMarket(
1693
+ perpMarket.quoteSpotMarketIndex
1694
+ );
1695
+ const strictOracle = new StrictOraclePrice(
1696
+ quoteOraclePriceData.price,
1697
+ quoteOraclePriceData.twap
1698
+ );
1699
+
1700
+ const positionUnrealizedPnl = calculatePositionPNL(
1701
+ perpMarket,
1702
+ perpPosition,
1703
+ true,
1704
+ oraclePriceData
1705
+ );
1706
+
1707
+ const tokenAmount = getTokenAmount(
1708
+ perpPosition.isolatedPositionScaledBalance ?? ZERO,
1709
+ quoteSpotMarket,
1710
+ SpotBalanceType.DEPOSIT
1711
+ );
1712
+
1713
+ const spotAssetValue = getStrictTokenValue(
1714
+ tokenAmount,
1715
+ quoteSpotMarket.decimals,
1716
+ strictOracle
1717
+ );
1718
+
1719
+ return {
1720
+ perpLiabilityValue: perpLiability,
1721
+ perpPnl: positionUnrealizedPnl,
1722
+ spotAssetValue,
1723
+ spotLiabilityValue: ZERO,
1724
+ };
1725
+ }
1726
+
1531
1727
  const perpLiability = this.getTotalPerpPositionLiability(
1532
1728
  marginCategory,
1533
1729
  undefined,
@@ -1817,32 +2013,83 @@ export class User {
1817
2013
  return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue);
1818
2014
  }
1819
2015
 
1820
- public canBeLiquidated(): {
1821
- canBeLiquidated: boolean;
1822
- marginRequirement: BN;
1823
- totalCollateral: BN;
2016
+ public canBeLiquidated(): AccountLiquidatableStatus & {
2017
+ isolatedPositions: Map<number, AccountLiquidatableStatus>;
1824
2018
  } {
1825
- const liquidationBuffer = this.getLiquidationBuffer();
1826
-
1827
- const totalCollateral = this.getTotalCollateral(
1828
- 'Maintenance',
1829
- undefined,
1830
- undefined,
1831
- liquidationBuffer
2019
+ // Deprecated signature retained for backward compatibility in type only
2020
+ // but implementation now delegates to the new Map-based API and returns cross margin status.
2021
+ const map = this.getLiquidationStatuses();
2022
+ const cross = map.get('cross');
2023
+ const isolatedPositions: Map<number, AccountLiquidatableStatus> = new Map(
2024
+ Array.from(map.entries())
2025
+ .filter(
2026
+ (e): e is [number, AccountLiquidatableStatus] => e[0] !== 'cross'
2027
+ )
2028
+ .map(([key, value]) => [key, value])
1832
2029
  );
2030
+ return cross
2031
+ ? { ...cross, isolatedPositions }
2032
+ : {
2033
+ canBeLiquidated: false,
2034
+ marginRequirement: ZERO,
2035
+ totalCollateral: ZERO,
2036
+ isolatedPositions,
2037
+ };
2038
+ }
1833
2039
 
1834
- const marginRequirement =
1835
- this.getMaintenanceMarginRequirement(liquidationBuffer);
1836
- const canBeLiquidated = totalCollateral.lt(marginRequirement);
2040
+ /**
2041
+ * New API: Returns liquidation status for cross and each isolated perp position.
2042
+ * Map keys:
2043
+ * - 'cross' for cross margin
2044
+ * - marketIndex (number) for each isolated perp position
2045
+ */
2046
+ public getLiquidationStatuses(
2047
+ marginCalc?: MarginCalculation
2048
+ ): Map<'cross' | number, AccountLiquidatableStatus> {
2049
+ // If not provided, use buffer-aware calc for canBeLiquidated checks
2050
+ if (!marginCalc) {
2051
+ const liquidationBufferMap = this.getLiquidationBuffer();
2052
+ marginCalc = this.getMarginCalculation('Maintenance', {
2053
+ liquidationBufferMap,
2054
+ });
2055
+ }
1837
2056
 
1838
- return {
1839
- canBeLiquidated,
1840
- marginRequirement,
1841
- totalCollateral,
1842
- };
2057
+ const result = new Map<'cross' | number, AccountLiquidatableStatus>();
2058
+
2059
+ // Cross margin status
2060
+ const crossTotalCollateral = marginCalc.totalCollateral;
2061
+ const crossMarginRequirement = marginCalc.marginRequirement;
2062
+ result.set('cross', {
2063
+ canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement),
2064
+ marginRequirement: crossMarginRequirement,
2065
+ totalCollateral: crossTotalCollateral,
2066
+ });
2067
+
2068
+ // Isolated positions status
2069
+ for (const [
2070
+ marketIndex,
2071
+ isoCalc,
2072
+ ] of marginCalc.isolatedMarginCalculations) {
2073
+ const isoTotalCollateral = isoCalc.totalCollateral;
2074
+ const isoMarginRequirement = isoCalc.marginRequirement;
2075
+ result.set(marketIndex, {
2076
+ canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement),
2077
+ marginRequirement: isoMarginRequirement,
2078
+ totalCollateral: isoTotalCollateral,
2079
+ });
2080
+ }
2081
+
2082
+ return result;
1843
2083
  }
1844
2084
 
1845
2085
  public isBeingLiquidated(): boolean {
2086
+ return (
2087
+ this.isCrossMarginBeingLiquidated() ||
2088
+ this.hasIsolatedPositionBeingLiquidated()
2089
+ );
2090
+ }
2091
+
2092
+ public isCrossMarginBeingLiquidated(): boolean {
1846
2093
  return (
1847
2094
  (this.getUserAccount().status &
1848
2095
  (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
@@ -1850,6 +2097,55 @@ export class User {
1850
2097
  );
1851
2098
  }
1852
2099
 
2100
+ /** Returns true if cross margin is currently below maintenance requirement (no buffer). */
2101
+ public canCrossMarginBeLiquidated(marginCalc?: MarginCalculation): boolean {
2102
+ const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
2103
+ return calc.totalCollateral.lt(calc.marginRequirement);
2104
+ }
2105
+
2106
+ public hasIsolatedPositionBeingLiquidated(): boolean {
2107
+ return this.getActivePerpPositions().some(
2108
+ (position) =>
2109
+ (position.positionFlag &
2110
+ (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) >
2111
+ 0
2112
+ );
2113
+ }
2114
+
2115
+ public isIsolatedPositionBeingLiquidated(perpMarketIndex: number): boolean {
2116
+ const position = this.getActivePerpPositions().find(
2117
+ (position) => position.marketIndex === perpMarketIndex
2118
+ );
2119
+
2120
+ return (
2121
+ (position?.positionFlag &
2122
+ (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) >
2123
+ 0
2124
+ );
2125
+ }
2126
+
2127
+ /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */
2128
+ public getLiquidatableIsolatedPositions(
2129
+ marginCalc?: MarginCalculation
2130
+ ): number[] {
2131
+ const liquidatableIsolatedPositions = [];
2132
+ const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
2133
+ for (const [marketIndex, isoCalc] of calc.isolatedMarginCalculations) {
2134
+ if (this.canIsolatedPositionMarginBeLiquidated(isoCalc)) {
2135
+ liquidatableIsolatedPositions.push(marketIndex);
2136
+ }
2137
+ }
2138
+ return liquidatableIsolatedPositions;
2139
+ }
2140
+
2141
+ public canIsolatedPositionMarginBeLiquidated(
2142
+ isolatedMarginCalculation: IsolatedMarginCalculation
2143
+ ): boolean {
2144
+ return isolatedMarginCalculation.totalCollateral.lt(
2145
+ isolatedMarginCalculation.marginRequirement
2146
+ );
2147
+ }
2148
+
1853
2149
  public hasStatus(status: UserStatus): boolean {
1854
2150
  return (this.getUserAccount().status & status) > 0;
1855
2151
  }
@@ -2006,13 +2302,68 @@ export class User {
2006
2302
  marginCategory: MarginCategory = 'Maintenance',
2007
2303
  includeOpenOrders = false,
2008
2304
  offsetCollateral = ZERO,
2009
- enteringHighLeverage = undefined
2305
+ enteringHighLeverage = false,
2306
+ marginType?: MarginType
2010
2307
  ): BN {
2308
+ const market = this.driftClient.getPerpMarketAccount(marketIndex);
2309
+
2310
+ const oracle =
2311
+ this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle;
2312
+
2313
+ const oraclePrice =
2314
+ this.driftClient.getOracleDataForPerpMarket(marketIndex).price;
2315
+
2316
+ const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex);
2317
+
2318
+ if (marginType === 'Isolated') {
2319
+ const marginCalculation = this.getMarginCalculation(marginCategory, {
2320
+ strict: false,
2321
+ includeOpenOrders,
2322
+ enteringHighLeverage,
2323
+ });
2324
+ const isolatedMarginCalculation =
2325
+ marginCalculation.isolatedMarginCalculations.get(marketIndex);
2326
+ if (!isolatedMarginCalculation) return new BN(-1);
2327
+ const { totalCollateral, marginRequirement } = isolatedMarginCalculation;
2328
+
2329
+ const freeCollateral = BN.max(
2330
+ ZERO,
2331
+ totalCollateral.sub(marginRequirement)
2332
+ ).add(offsetCollateral);
2333
+
2334
+ const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp(
2335
+ market,
2336
+ currentPerpPosition,
2337
+ positionBaseSizeChange,
2338
+ oraclePrice,
2339
+ marginCategory,
2340
+ includeOpenOrders,
2341
+ enteringHighLeverage
2342
+ );
2343
+
2344
+ if (freeCollateralDelta.eq(ZERO)) {
2345
+ return new BN(-1);
2346
+ }
2347
+
2348
+ const liqPriceDelta = freeCollateral
2349
+ .mul(QUOTE_PRECISION)
2350
+ .div(freeCollateralDelta);
2351
+
2352
+ const liqPrice = oraclePrice.sub(liqPriceDelta);
2353
+
2354
+ if (liqPrice.lt(ZERO)) {
2355
+ return new BN(-1);
2356
+ }
2357
+
2358
+ return liqPrice;
2359
+ }
2360
+
2011
2361
  const totalCollateral = this.getTotalCollateral(
2012
2362
  marginCategory,
2013
2363
  false,
2014
2364
  includeOpenOrders
2015
2365
  );
2366
+
2016
2367
  const marginRequirement = this.getMarginRequirement(
2017
2368
  marginCategory,
2018
2369
  undefined,
@@ -2020,20 +2371,12 @@ export class User {
2020
2371
  includeOpenOrders,
2021
2372
  enteringHighLeverage
2022
2373
  );
2374
+
2023
2375
  let freeCollateral = BN.max(
2024
2376
  ZERO,
2025
2377
  totalCollateral.sub(marginRequirement)
2026
2378
  ).add(offsetCollateral);
2027
2379
 
2028
- const oracle =
2029
- this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle;
2030
-
2031
- const oraclePrice =
2032
- this.driftClient.getOracleDataForPerpMarket(marketIndex).price;
2033
-
2034
- const market = this.driftClient.getPerpMarketAccount(marketIndex);
2035
- const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex);
2036
-
2037
2380
  positionBaseSizeChange = standardizeBaseAssetAmount(
2038
2381
  positionBaseSizeChange,
2039
2382
  market.amm.orderStepSize
@@ -3915,4 +4258,311 @@ export class User {
3915
4258
  activeSpotPositions: activeSpotMarkets,
3916
4259
  };
3917
4260
  }
4261
+
4262
+ /**
4263
+ * Compute a consolidated margin snapshot once, without caching.
4264
+ * Consumers can use this to avoid duplicating work across separate calls.
4265
+ */
4266
+ public getMarginCalculation(
4267
+ marginCategory: MarginCategory = 'Initial',
4268
+ opts?: {
4269
+ strict?: boolean; // mirror StrictOraclePrice application
4270
+ includeOpenOrders?: boolean;
4271
+ enteringHighLeverage?: boolean;
4272
+ liquidationBufferMap?: Map<number | 'cross', BN>; // margin_buffer analog for buffer mode
4273
+ }
4274
+ ): MarginCalculation {
4275
+ const strict = opts?.strict ?? false;
4276
+ const enteringHighLeverage = opts?.enteringHighLeverage ?? false;
4277
+ const liquidationBufferMap = opts?.liquidationBufferMap ?? new Map();
4278
+ const includeOpenOrders = opts?.includeOpenOrders ?? true;
4279
+
4280
+ // Equivalent to on-chain user_custom_margin_ratio
4281
+ const userCustomMarginRatio =
4282
+ marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0;
4283
+
4284
+ // Initialize calc via JS mirror of Rust/on-chain MarginCalculation
4285
+ const isolatedMarginBuffers = new Map<number, BN>();
4286
+ for (const [
4287
+ marketIndex,
4288
+ isolatedMarginBuffer,
4289
+ ] of opts?.liquidationBufferMap ?? new Map()) {
4290
+ if (marketIndex !== 'cross') {
4291
+ isolatedMarginBuffers.set(marketIndex, isolatedMarginBuffer);
4292
+ }
4293
+ }
4294
+ const ctx = MarginContext.standard(marginCategory)
4295
+ .strictMode(strict)
4296
+ .setCrossMarginBuffer(opts?.liquidationBufferMap?.get('cross') ?? ZERO)
4297
+ .setIsolatedMarginBuffers(isolatedMarginBuffers);
4298
+ const calc = new MarginCalculation(ctx);
4299
+
4300
+ // SPOT POSITIONS
4301
+ for (const spotPosition of this.getUserAccount().spotPositions) {
4302
+ if (isSpotPositionAvailable(spotPosition)) continue;
4303
+
4304
+ const isQuote = spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX;
4305
+
4306
+ const spotMarket = this.driftClient.getSpotMarketAccount(
4307
+ spotPosition.marketIndex
4308
+ );
4309
+ const oraclePriceData = this.getOracleDataForSpotMarket(
4310
+ spotPosition.marketIndex
4311
+ );
4312
+ const twap5 = strict
4313
+ ? calculateLiveOracleTwap(
4314
+ spotMarket.historicalOracleData,
4315
+ oraclePriceData,
4316
+ new BN(Math.floor(Date.now() / 1000)),
4317
+ FIVE_MINUTE
4318
+ )
4319
+ : undefined;
4320
+ const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5);
4321
+
4322
+ if (isQuote) {
4323
+ const tokenAmount = getSignedTokenAmount(
4324
+ getTokenAmount(
4325
+ spotPosition.scaledBalance,
4326
+ spotMarket,
4327
+ spotPosition.balanceType
4328
+ ),
4329
+ spotPosition.balanceType
4330
+ );
4331
+ if (isVariant(spotPosition.balanceType, 'deposit')) {
4332
+ // add deposit value to total collateral
4333
+ const weightedTokenValue = this.getSpotAssetValue(
4334
+ tokenAmount,
4335
+ strictOracle,
4336
+ spotMarket,
4337
+ marginCategory
4338
+ );
4339
+ calc.addCrossMarginTotalCollateral(weightedTokenValue);
4340
+ } else {
4341
+ // borrow on quote contributes to margin requirement
4342
+ const tokenValueAbs = this.getSpotLiabilityValue(
4343
+ tokenAmount,
4344
+ strictOracle,
4345
+ spotMarket,
4346
+ marginCategory,
4347
+ liquidationBufferMap.get('cross') ?? new BN(0)
4348
+ ).abs();
4349
+ calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs);
4350
+ }
4351
+ continue;
4352
+ }
4353
+
4354
+ // Non-quote spot: worst-case simulation
4355
+ const {
4356
+ tokenAmount: worstCaseTokenAmount,
4357
+ ordersValue: worstCaseOrdersValue,
4358
+ } = getWorstCaseTokenAmounts(
4359
+ spotPosition,
4360
+ spotMarket,
4361
+ strictOracle,
4362
+ marginCategory,
4363
+ userCustomMarginRatio,
4364
+ includeOpenOrders
4365
+ // false
4366
+ );
4367
+
4368
+ if (includeOpenOrders) {
4369
+ // open order IM
4370
+ calc.addCrossMarginRequirement(
4371
+ new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT),
4372
+ ZERO
4373
+ );
4374
+ }
4375
+
4376
+ if (worstCaseTokenAmount.gt(ZERO)) {
4377
+ const baseAssetValue = this.getSpotAssetValue(
4378
+ worstCaseTokenAmount,
4379
+ strictOracle,
4380
+ spotMarket,
4381
+ marginCategory
4382
+ );
4383
+ // asset side increases total collateral (weighted)
4384
+ calc.addCrossMarginTotalCollateral(baseAssetValue);
4385
+ } else if (worstCaseTokenAmount.lt(ZERO)) {
4386
+ // liability side increases margin requirement (weighted >= abs(token_value))
4387
+ const getSpotLiabilityValue = this.getSpotLiabilityValue(
4388
+ worstCaseTokenAmount,
4389
+ strictOracle,
4390
+ spotMarket,
4391
+ marginCategory,
4392
+ liquidationBufferMap.get('cross')
4393
+ );
4394
+
4395
+ calc.addCrossMarginRequirement(
4396
+ getSpotLiabilityValue.abs(),
4397
+ getSpotLiabilityValue.abs()
4398
+ );
4399
+ }
4400
+
4401
+ // orders value contributes to collateral or requirement
4402
+ if (worstCaseOrdersValue.gt(ZERO)) {
4403
+ calc.addCrossMarginTotalCollateral(worstCaseOrdersValue);
4404
+ } else if (worstCaseOrdersValue.lt(ZERO)) {
4405
+ const absVal = worstCaseOrdersValue.abs();
4406
+ calc.addCrossMarginRequirement(absVal, absVal);
4407
+ }
4408
+ }
4409
+
4410
+ // PERP POSITIONS
4411
+ for (const marketPosition of this.getActivePerpPositions()) {
4412
+ const market = this.driftClient.getPerpMarketAccount(
4413
+ marketPosition.marketIndex
4414
+ );
4415
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
4416
+ market.quoteSpotMarketIndex
4417
+ );
4418
+ const quoteOraclePriceData = this.getOracleDataForSpotMarket(
4419
+ market.quoteSpotMarketIndex
4420
+ );
4421
+ const oraclePriceData = this.getMMOracleDataForPerpMarket(
4422
+ market.marketIndex
4423
+ );
4424
+
4425
+ const nonMmmOraclePriceData = this.getOracleDataForPerpMarket(
4426
+ market.marketIndex
4427
+ );
4428
+
4429
+ // Worst-case perp liability and weighted pnl
4430
+ const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } =
4431
+ calculateWorstCasePerpLiabilityValue(
4432
+ marketPosition,
4433
+ market,
4434
+ nonMmmOraclePriceData.price,
4435
+ includeOpenOrders
4436
+ );
4437
+
4438
+ // margin ratio for this perp
4439
+ const customMarginRatio = Math.max(
4440
+ userCustomMarginRatio,
4441
+ marketPosition.maxMarginRatio
4442
+ );
4443
+ let marginRatio = new BN(
4444
+ calculateMarketMarginRatio(
4445
+ market,
4446
+ worstCaseBaseAssetAmount.abs(),
4447
+ marginCategory,
4448
+ customMarginRatio,
4449
+ this.isHighLeverageMode(marginCategory) || enteringHighLeverage
4450
+ )
4451
+ );
4452
+ if (isVariant(market.status, 'settlement')) {
4453
+ marginRatio = ZERO;
4454
+ }
4455
+
4456
+ // convert liability to quote value and apply margin ratio
4457
+ const quotePrice = strict
4458
+ ? BN.max(
4459
+ quoteOraclePriceData.price,
4460
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4461
+ )
4462
+ : quoteOraclePriceData.price;
4463
+ let perpMarginRequirement = worstCaseLiabilityValue
4464
+ .mul(quotePrice)
4465
+ .div(PRICE_PRECISION)
4466
+ .mul(marginRatio)
4467
+ .div(MARGIN_PRECISION);
4468
+ // add open orders IM
4469
+ if (includeOpenOrders) {
4470
+ perpMarginRequirement = perpMarginRequirement.add(
4471
+ new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
4472
+ );
4473
+ }
4474
+
4475
+ // weighted unrealized pnl
4476
+ let positionUnrealizedPnl = calculatePositionPNL(
4477
+ market,
4478
+ marketPosition,
4479
+ true,
4480
+ oraclePriceData
4481
+ );
4482
+ let pnlQuotePrice: BN;
4483
+ if (strict && positionUnrealizedPnl.gt(ZERO)) {
4484
+ pnlQuotePrice = BN.min(
4485
+ quoteOraclePriceData.price,
4486
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4487
+ );
4488
+ } else if (strict && positionUnrealizedPnl.lt(ZERO)) {
4489
+ pnlQuotePrice = BN.max(
4490
+ quoteOraclePriceData.price,
4491
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4492
+ );
4493
+ } else {
4494
+ pnlQuotePrice = quoteOraclePriceData.price;
4495
+ }
4496
+ positionUnrealizedPnl = positionUnrealizedPnl
4497
+ .mul(pnlQuotePrice)
4498
+ .div(PRICE_PRECISION);
4499
+
4500
+ if (marginCategory !== undefined) {
4501
+ if (positionUnrealizedPnl.gt(ZERO)) {
4502
+ positionUnrealizedPnl = positionUnrealizedPnl
4503
+ .mul(
4504
+ calculateUnrealizedAssetWeight(
4505
+ market,
4506
+ quoteSpotMarket,
4507
+ positionUnrealizedPnl,
4508
+ marginCategory,
4509
+ oraclePriceData
4510
+ )
4511
+ )
4512
+ .div(new BN(SPOT_MARKET_WEIGHT_PRECISION));
4513
+ }
4514
+ }
4515
+
4516
+ // Add perp contribution: isolated vs cross
4517
+ const isIsolated = this.isPerpPositionIsolated(marketPosition);
4518
+ if (isIsolated) {
4519
+ // derive isolated quote deposit value, mirroring on-chain logic
4520
+ let depositValue = ZERO;
4521
+ if (marketPosition.isolatedPositionScaledBalance?.gt(ZERO)) {
4522
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
4523
+ market.quoteSpotMarketIndex
4524
+ );
4525
+ const quoteOraclePriceData = this.getOracleDataForSpotMarket(
4526
+ market.quoteSpotMarketIndex
4527
+ );
4528
+ const strictQuote = new StrictOraclePrice(
4529
+ quoteOraclePriceData.price,
4530
+ strict
4531
+ ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4532
+ : undefined
4533
+ );
4534
+ const quoteTokenAmount = getTokenAmount(
4535
+ marketPosition.isolatedPositionScaledBalance ?? ZERO,
4536
+ quoteSpotMarket,
4537
+ SpotBalanceType.DEPOSIT
4538
+ );
4539
+ depositValue = getStrictTokenValue(
4540
+ quoteTokenAmount,
4541
+ quoteSpotMarket.decimals,
4542
+ strictQuote
4543
+ );
4544
+ }
4545
+ calc.addIsolatedMarginCalculation(
4546
+ market.marketIndex,
4547
+ depositValue,
4548
+ positionUnrealizedPnl,
4549
+ worstCaseLiabilityValue,
4550
+ perpMarginRequirement
4551
+ );
4552
+ calc.addPerpLiabilityValue(worstCaseLiabilityValue);
4553
+ } else {
4554
+ // cross: add to global requirement and collateral
4555
+ calc.addCrossMarginRequirement(
4556
+ perpMarginRequirement,
4557
+ worstCaseLiabilityValue
4558
+ );
4559
+ calc.addCrossMarginTotalCollateral(positionUnrealizedPnl);
4560
+ }
4561
+ }
4562
+ return calc;
4563
+ }
4564
+
4565
+ private isPerpPositionIsolated(perpPosition: PerpPosition): boolean {
4566
+ return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0;
4567
+ }
3918
4568
  }