@drift-labs/sdk 2.145.0 → 2.146.0-alpha.13

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 (99) hide show
  1. package/.env +4 -0
  2. package/VERSION +1 -1
  3. package/lib/browser/accounts/grpcMultiUserAccountSubscriber.js +8 -1
  4. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.d.ts +99 -7
  5. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.js +435 -144
  6. package/lib/browser/adminClient.d.ts +5 -1
  7. package/lib/browser/adminClient.js +57 -23
  8. package/lib/browser/constants/numericConstants.d.ts +2 -0
  9. package/lib/browser/constants/numericConstants.js +5 -1
  10. package/lib/browser/constants/perpMarkets.js +0 -2
  11. package/lib/browser/decode/user.js +4 -0
  12. package/lib/browser/driftClient.d.ts +25 -10
  13. package/lib/browser/driftClient.js +238 -41
  14. package/lib/browser/driftClientConfig.d.ts +7 -2
  15. package/lib/browser/idl/drift.json +245 -22
  16. package/lib/browser/index.d.ts +4 -0
  17. package/lib/browser/index.js +9 -1
  18. package/lib/browser/marginCalculation.d.ts +86 -0
  19. package/lib/browser/marginCalculation.js +209 -0
  20. package/lib/browser/math/margin.d.ts +1 -1
  21. package/lib/browser/math/margin.js +8 -1
  22. package/lib/browser/math/position.d.ts +1 -0
  23. package/lib/browser/math/position.js +10 -2
  24. package/lib/browser/math/spotPosition.d.ts +1 -1
  25. package/lib/browser/math/spotPosition.js +3 -2
  26. package/lib/browser/math/superStake.d.ts +3 -2
  27. package/lib/browser/types.d.ts +13 -0
  28. package/lib/browser/types.js +12 -1
  29. package/lib/browser/user.d.ts +59 -11
  30. package/lib/browser/user.js +348 -43
  31. package/lib/node/accounts/grpcMultiUserAccountSubscriber.d.ts.map +1 -1
  32. package/lib/node/accounts/grpcMultiUserAccountSubscriber.js +8 -1
  33. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts +99 -7
  34. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts.map +1 -1
  35. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.js +435 -144
  36. package/lib/node/adminClient.d.ts +5 -1
  37. package/lib/node/adminClient.d.ts.map +1 -1
  38. package/lib/node/adminClient.js +57 -23
  39. package/lib/node/constants/numericConstants.d.ts +2 -0
  40. package/lib/node/constants/numericConstants.d.ts.map +1 -1
  41. package/lib/node/constants/numericConstants.js +5 -1
  42. package/lib/node/constants/perpMarkets.d.ts.map +1 -1
  43. package/lib/node/constants/perpMarkets.js +0 -2
  44. package/lib/node/decode/user.d.ts.map +1 -1
  45. package/lib/node/decode/user.js +4 -0
  46. package/lib/node/driftClient.d.ts +25 -10
  47. package/lib/node/driftClient.d.ts.map +1 -1
  48. package/lib/node/driftClient.js +238 -41
  49. package/lib/node/driftClientConfig.d.ts +7 -2
  50. package/lib/node/driftClientConfig.d.ts.map +1 -1
  51. package/lib/node/idl/drift.json +245 -22
  52. package/lib/node/index.d.ts +4 -0
  53. package/lib/node/index.d.ts.map +1 -1
  54. package/lib/node/index.js +9 -1
  55. package/lib/node/marginCalculation.d.ts +87 -0
  56. package/lib/node/marginCalculation.d.ts.map +1 -0
  57. package/lib/node/marginCalculation.js +209 -0
  58. package/lib/node/math/margin.d.ts +1 -1
  59. package/lib/node/math/margin.d.ts.map +1 -1
  60. package/lib/node/math/margin.js +8 -1
  61. package/lib/node/math/position.d.ts +1 -0
  62. package/lib/node/math/position.d.ts.map +1 -1
  63. package/lib/node/math/position.js +10 -2
  64. package/lib/node/math/spotPosition.d.ts +1 -1
  65. package/lib/node/math/spotPosition.d.ts.map +1 -1
  66. package/lib/node/math/spotPosition.js +3 -2
  67. package/lib/node/math/superStake.d.ts +3 -2
  68. package/lib/node/math/superStake.d.ts.map +1 -1
  69. package/lib/node/types.d.ts +13 -0
  70. package/lib/node/types.d.ts.map +1 -1
  71. package/lib/node/types.js +12 -1
  72. package/lib/node/user.d.ts +59 -11
  73. package/lib/node/user.d.ts.map +1 -1
  74. package/lib/node/user.js +348 -43
  75. package/package.json +1 -1
  76. package/scripts/deposit-isolated-positions.ts +110 -0
  77. package/scripts/single-grpc-client-test.ts +71 -21
  78. package/scripts/withdraw-isolated-positions.ts +174 -0
  79. package/src/accounts/grpcMultiUserAccountSubscriber.ts +8 -1
  80. package/src/accounts/webSocketProgramAccountSubscriberV2.ts +566 -167
  81. package/src/adminClient.ts +74 -25
  82. package/src/constants/numericConstants.ts +5 -0
  83. package/src/constants/perpMarkets.ts +0 -3
  84. package/src/decode/user.ts +7 -1
  85. package/src/driftClient.ts +465 -52
  86. package/src/driftClientConfig.ts +15 -8
  87. package/src/idl/drift.json +246 -23
  88. package/src/index.ts +4 -0
  89. package/src/margin/README.md +143 -0
  90. package/src/marginCalculation.ts +306 -0
  91. package/src/math/margin.ts +13 -1
  92. package/src/math/position.ts +12 -2
  93. package/src/math/spotPosition.ts +6 -2
  94. package/src/types.ts +16 -0
  95. package/src/user.ts +623 -81
  96. package/tests/amm/test.ts +1 -1
  97. package/tests/dlob/helpers.ts +6 -3
  98. package/tests/user/getMarginCalculation.ts +405 -0
  99. package/tests/user/test.ts +0 -7
package/src/user.ts CHANGED
@@ -68,6 +68,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade';
68
68
  import {
69
69
  MarketType,
70
70
  PositionDirection,
71
+ PositionFlag,
71
72
  SpotBalanceType,
72
73
  SpotMarketAccount,
73
74
  } from './types';
@@ -106,6 +107,9 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice';
106
107
 
107
108
  import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel';
108
109
  import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber';
110
+ import { MarginCalculation, MarginContext } from './marginCalculation';
111
+
112
+ export type MarginType = 'Cross' | 'Isolated';
109
113
 
110
114
  export class User {
111
115
  driftClient: DriftClient;
@@ -338,9 +342,27 @@ export class User {
338
342
  lastQuoteAssetAmountPerLp: ZERO,
339
343
  perLpBase: 0,
340
344
  maxMarginRatio: 0,
345
+ positionFlag: 0,
346
+ isolatedPositionScaledBalance: ZERO,
341
347
  };
342
348
  }
343
349
 
350
+ public getIsolatePerpPositionTokenAmount(perpMarketIndex: number): BN {
351
+ const perpPosition = this.getPerpPosition(perpMarketIndex);
352
+ const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex);
353
+ const spotMarket = this.driftClient.getSpotMarketAccount(
354
+ perpMarket.quoteSpotMarketIndex
355
+ );
356
+ if (perpPosition === undefined) {
357
+ return ZERO;
358
+ }
359
+ return getTokenAmount(
360
+ perpPosition.isolatedPositionScaledBalance,
361
+ spotMarket,
362
+ SpotBalanceType.DEPOSIT
363
+ );
364
+ }
365
+
344
366
  public getClonedPosition(position: PerpPosition): PerpPosition {
345
367
  const clonedPosition = Object.assign({}, position);
346
368
  return clonedPosition;
@@ -513,62 +535,113 @@ export class User {
513
535
  */
514
536
  public getFreeCollateral(
515
537
  marginCategory: MarginCategory = 'Initial',
516
- enterHighLeverageMode = undefined
538
+ enterHighLeverageMode = false,
539
+ perpMarketIndex?: number
517
540
  ): BN {
518
- const totalCollateral = this.getTotalCollateral(marginCategory, true);
519
- const marginRequirement =
520
- marginCategory === 'Initial'
521
- ? this.getInitialMarginRequirement(enterHighLeverageMode)
522
- : this.getMaintenanceMarginRequirement();
523
- const freeCollateral = totalCollateral.sub(marginRequirement);
524
- return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
541
+ const marginCalc = this.getMarginCalculation(marginCategory, {
542
+ enteringHighLeverage: enterHighLeverageMode,
543
+ });
544
+
545
+ if (perpMarketIndex !== undefined) {
546
+ return marginCalc.getIsolatedFreeCollateral(perpMarketIndex);
547
+ } else {
548
+ return marginCalc.getCrossFreeCollateral();
549
+ }
525
550
  }
526
551
 
527
552
  /**
528
- * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION
553
+ * @deprecated Use the overload that includes { marginType, perpMarketIndex }
529
554
  */
530
555
  public getMarginRequirement(
531
556
  marginCategory: MarginCategory,
532
557
  liquidationBuffer?: BN,
533
- strict = false,
534
- includeOpenOrders = true,
535
- enteringHighLeverage = undefined
558
+ strict?: boolean,
559
+ includeOpenOrders?: boolean,
560
+ enteringHighLeverage?: boolean
561
+ ): BN;
562
+
563
+ /**
564
+ * Calculates the margin requirement based on the specified parameters.
565
+ *
566
+ * @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance').
567
+ * @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios.
568
+ * @param strict - Optional flag to enforce strict margin calculations.
569
+ * @param includeOpenOrders - Optional flag to include open orders in the margin calculation.
570
+ * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode.
571
+ * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'.
572
+ *
573
+ * @returns The calculated margin requirement as a BN (BigNumber).
574
+ */
575
+ public getMarginRequirement(
576
+ marginCategory: MarginCategory,
577
+ liquidationBuffer?: BN,
578
+ strict?: boolean,
579
+ includeOpenOrders?: boolean,
580
+ enteringHighLeverage?: boolean,
581
+ perpMarketIndex?: number
582
+ ): BN;
583
+
584
+ public getMarginRequirement(
585
+ marginCategory: MarginCategory,
586
+ liquidationBuffer?: BN,
587
+ strict?: boolean,
588
+ includeOpenOrders?: boolean,
589
+ enteringHighLeverage?: boolean,
590
+ perpMarketIndex?: number
536
591
  ): BN {
537
- return this.getTotalPerpPositionLiability(
538
- marginCategory,
539
- liquidationBuffer,
540
- includeOpenOrders,
592
+ const marginCalc = this.getMarginCalculation(marginCategory, {
541
593
  strict,
542
- enteringHighLeverage
543
- ).add(
544
- this.getSpotMarketLiabilityValue(
545
- undefined,
546
- marginCategory,
547
- liquidationBuffer,
548
- includeOpenOrders,
549
- strict
550
- )
551
- );
594
+ includeOpenOrders,
595
+ enteringHighLeverage,
596
+ liquidationBuffer,
597
+ });
598
+
599
+ // If perpMarketIndex is provided, compute only for that market index
600
+ if (perpMarketIndex !== undefined) {
601
+ const isolatedMarginCalculation =
602
+ marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
603
+ const { marginRequirement } = isolatedMarginCalculation;
604
+
605
+ return marginRequirement;
606
+ }
607
+
608
+ // Default: Cross margin requirement
609
+ // TODO: should we be using plus buffer sometimes?
610
+ return marginCalc.marginRequirement;
552
611
  }
553
612
 
554
613
  /**
555
614
  * @returns The initial margin requirement in USDC. : QUOTE_PRECISION
556
615
  */
557
- public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN {
616
+ public getInitialMarginRequirement(
617
+ enterHighLeverageMode = false,
618
+ perpMarketIndex?: number
619
+ ): BN {
558
620
  return this.getMarginRequirement(
559
621
  'Initial',
560
622
  undefined,
561
- true,
623
+ false,
562
624
  undefined,
563
- enterHighLeverageMode
625
+ enterHighLeverageMode,
626
+ perpMarketIndex
564
627
  );
565
628
  }
566
629
 
567
630
  /**
568
631
  * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION
569
632
  */
570
- public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN {
571
- return this.getMarginRequirement('Maintenance', liquidationBuffer);
633
+ public getMaintenanceMarginRequirement(
634
+ liquidationBuffer?: BN,
635
+ perpMarketIndex?: number
636
+ ): BN {
637
+ return this.getMarginRequirement(
638
+ 'Maintenance',
639
+ liquidationBuffer,
640
+ true, // strict default
641
+ true, // includeOpenOrders default
642
+ false, // enteringHighLeverage default
643
+ perpMarketIndex
644
+ );
572
645
  }
573
646
 
574
647
  public getActivePerpPositionsForUserAccount(
@@ -578,7 +651,8 @@ export class User {
578
651
  (pos) =>
579
652
  !pos.baseAssetAmount.eq(ZERO) ||
580
653
  !pos.quoteAssetAmount.eq(ZERO) ||
581
- !(pos.openOrders == 0)
654
+ !(pos.openOrders == 0) ||
655
+ pos.isolatedPositionScaledBalance.gt(ZERO)
582
656
  );
583
657
  }
584
658
 
@@ -639,6 +713,7 @@ export class User {
639
713
  const market = this.driftClient.getPerpMarketAccount(
640
714
  perpPosition.marketIndex
641
715
  );
716
+ if (!market) return unrealizedPnl;
642
717
  const oraclePriceData = this.getMMOracleDataForPerpMarket(
643
718
  market.marketIndex
644
719
  );
@@ -1151,22 +1226,21 @@ export class User {
1151
1226
  marginCategory: MarginCategory = 'Initial',
1152
1227
  strict = false,
1153
1228
  includeOpenOrders = true,
1154
- liquidationBuffer?: BN
1229
+ liquidationBuffer?: BN,
1230
+ perpMarketIndex?: number
1155
1231
  ): BN {
1156
- return this.getSpotMarketAssetValue(
1157
- undefined,
1158
- marginCategory,
1232
+ const marginCalc = this.getMarginCalculation(marginCategory, {
1233
+ strict,
1159
1234
  includeOpenOrders,
1160
- strict
1161
- ).add(
1162
- this.getUnrealizedPNL(
1163
- true,
1164
- undefined,
1165
- marginCategory,
1166
- strict,
1167
- liquidationBuffer
1168
- )
1169
- );
1235
+ liquidationBuffer,
1236
+ });
1237
+
1238
+ if (perpMarketIndex !== undefined) {
1239
+ return marginCalc.isolatedMarginCalculations.get(perpMarketIndex)
1240
+ .totalCollateral;
1241
+ }
1242
+
1243
+ return marginCalc.totalCollateral;
1170
1244
  }
1171
1245
 
1172
1246
  public getLiquidationBuffer(): BN | undefined {
@@ -1184,13 +1258,26 @@ export class User {
1184
1258
  * calculates User Health by comparing total collateral and maint. margin requirement
1185
1259
  * @returns : number (value from [0, 100])
1186
1260
  */
1187
- public getHealth(): number {
1188
- if (this.isBeingLiquidated()) {
1261
+ public getHealth(perpMarketIndex?: number): number {
1262
+ const marginCalc = this.getMarginCalculation('Maintenance');
1263
+ if (this.isCrossMarginBeingLiquidated(marginCalc) && !perpMarketIndex) {
1189
1264
  return 0;
1190
1265
  }
1191
1266
 
1192
- const totalCollateral = this.getTotalCollateral('Maintenance');
1193
- const maintenanceMarginReq = this.getMaintenanceMarginRequirement();
1267
+ let totalCollateral: BN;
1268
+ let maintenanceMarginReq: BN;
1269
+
1270
+ if (perpMarketIndex) {
1271
+ const isolatedMarginCalc =
1272
+ marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
1273
+ if (isolatedMarginCalc) {
1274
+ totalCollateral = isolatedMarginCalc.totalCollateral;
1275
+ maintenanceMarginReq = isolatedMarginCalc.marginRequirement;
1276
+ }
1277
+ } else {
1278
+ totalCollateral = marginCalc.totalCollateral;
1279
+ maintenanceMarginReq = marginCalc.marginRequirement;
1280
+ }
1194
1281
 
1195
1282
  let health: number;
1196
1283
 
@@ -1226,6 +1313,8 @@ export class User {
1226
1313
  perpPosition.marketIndex
1227
1314
  );
1228
1315
 
1316
+ if (!market) return ZERO;
1317
+
1229
1318
  let valuationPrice = this.getOracleDataForPerpMarket(
1230
1319
  market.marketIndex
1231
1320
  ).price;
@@ -1489,9 +1578,9 @@ export class User {
1489
1578
  * calculates current user leverage which is (total liability size) / (net asset value)
1490
1579
  * @returns : Precision TEN_THOUSAND
1491
1580
  */
1492
- public getLeverage(includeOpenOrders = true): BN {
1581
+ public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN {
1493
1582
  return this.calculateLeverageFromComponents(
1494
- this.getLeverageComponents(includeOpenOrders)
1583
+ this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex)
1495
1584
  );
1496
1585
  }
1497
1586
 
@@ -1519,13 +1608,61 @@ export class User {
1519
1608
 
1520
1609
  getLeverageComponents(
1521
1610
  includeOpenOrders = true,
1522
- marginCategory: MarginCategory = undefined
1611
+ marginCategory: MarginCategory = undefined,
1612
+ perpMarketIndex?: number
1523
1613
  ): {
1524
1614
  perpLiabilityValue: BN;
1525
1615
  perpPnl: BN;
1526
1616
  spotAssetValue: BN;
1527
1617
  spotLiabilityValue: BN;
1528
1618
  } {
1619
+ if (perpMarketIndex) {
1620
+ const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex);
1621
+ const perpLiability = this.calculateWeightedPerpPositionLiability(
1622
+ perpPosition,
1623
+ marginCategory,
1624
+ undefined,
1625
+ includeOpenOrders
1626
+ );
1627
+ const perpMarket = this.driftClient.getPerpMarketAccount(
1628
+ perpPosition.marketIndex
1629
+ );
1630
+
1631
+ const oraclePriceData = this.getOracleDataForPerpMarket(
1632
+ perpPosition.marketIndex
1633
+ );
1634
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
1635
+ perpMarket.quoteSpotMarketIndex
1636
+ );
1637
+ const quoteOraclePriceData = this.getOracleDataForSpotMarket(
1638
+ perpMarket.quoteSpotMarketIndex
1639
+ );
1640
+ const strictOracle = new StrictOraclePrice(
1641
+ quoteOraclePriceData.price,
1642
+ quoteOraclePriceData.twap
1643
+ );
1644
+
1645
+ const positionUnrealizedPnl = calculatePositionPNL(
1646
+ perpMarket,
1647
+ perpPosition,
1648
+ true,
1649
+ oraclePriceData
1650
+ );
1651
+
1652
+ const spotAssetValue = getStrictTokenValue(
1653
+ perpPosition.isolatedPositionScaledBalance,
1654
+ quoteSpotMarket.decimals,
1655
+ strictOracle
1656
+ );
1657
+
1658
+ return {
1659
+ perpLiabilityValue: perpLiability,
1660
+ perpPnl: positionUnrealizedPnl,
1661
+ spotAssetValue,
1662
+ spotLiabilityValue: ZERO,
1663
+ };
1664
+ }
1665
+
1529
1666
  const perpLiability = this.getTotalPerpPositionLiability(
1530
1667
  marginCategory,
1531
1668
  undefined,
@@ -1819,35 +1956,113 @@ export class User {
1819
1956
  canBeLiquidated: boolean;
1820
1957
  marginRequirement: BN;
1821
1958
  totalCollateral: BN;
1959
+ liquidationStatuses: Map<
1960
+ 'cross' | number,
1961
+ { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }
1962
+ >;
1822
1963
  } {
1823
- const liquidationBuffer = this.getLiquidationBuffer();
1964
+ // Deprecated signature retained for backward compatibility in type only
1965
+ // but implementation now delegates to the new Map-based API and returns cross margin status.
1966
+ const map = this.getLiquidationStatuses();
1967
+ const cross = map.get('cross');
1968
+ return cross
1969
+ ? { ...cross, liquidationStatuses: map }
1970
+ : {
1971
+ canBeLiquidated: false,
1972
+ marginRequirement: ZERO,
1973
+ totalCollateral: ZERO,
1974
+ liquidationStatuses: map,
1975
+ };
1976
+ }
1824
1977
 
1825
- const totalCollateral = this.getTotalCollateral(
1826
- 'Maintenance',
1827
- undefined,
1828
- undefined,
1829
- liquidationBuffer
1830
- );
1978
+ /**
1979
+ * New API: Returns liquidation status for cross and each isolated perp position.
1980
+ * Map keys:
1981
+ * - 'cross' for cross margin
1982
+ * - marketIndex (number) for each isolated perp position
1983
+ */
1984
+ public getLiquidationStatuses(
1985
+ marginCalc?: MarginCalculation
1986
+ ): Map<
1987
+ 'cross' | number,
1988
+ { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }
1989
+ > {
1990
+ // If not provided, use buffer-aware calc for canBeLiquidated checks
1991
+ if (!marginCalc) {
1992
+ const liquidationBuffer = this.getLiquidationBuffer();
1993
+ marginCalc = this.getMarginCalculation('Maintenance', {
1994
+ liquidationBuffer,
1995
+ });
1996
+ }
1831
1997
 
1832
- const marginRequirement =
1833
- this.getMaintenanceMarginRequirement(liquidationBuffer);
1834
- const canBeLiquidated = totalCollateral.lt(marginRequirement);
1998
+ const result = new Map<
1999
+ 'cross' | number,
2000
+ {
2001
+ canBeLiquidated: boolean;
2002
+ marginRequirement: BN;
2003
+ totalCollateral: BN;
2004
+ }
2005
+ >();
2006
+
2007
+ // Cross margin status
2008
+ const crossTotalCollateral = marginCalc.totalCollateral;
2009
+ const crossMarginRequirement = marginCalc.marginRequirement;
2010
+ result.set('cross', {
2011
+ canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement),
2012
+ marginRequirement: crossMarginRequirement,
2013
+ totalCollateral: crossTotalCollateral,
2014
+ });
1835
2015
 
1836
- return {
1837
- canBeLiquidated,
1838
- marginRequirement,
1839
- totalCollateral,
1840
- };
2016
+ // Isolated positions status
2017
+ for (const [
2018
+ marketIndex,
2019
+ isoCalc,
2020
+ ] of marginCalc.isolatedMarginCalculations) {
2021
+ const isoTotalCollateral = isoCalc.totalCollateral;
2022
+ const isoMarginRequirement = isoCalc.marginRequirement;
2023
+ result.set(marketIndex, {
2024
+ canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement),
2025
+ marginRequirement: isoMarginRequirement,
2026
+ totalCollateral: isoTotalCollateral,
2027
+ });
2028
+ }
2029
+
2030
+ return result;
1841
2031
  }
1842
2032
 
1843
- public isBeingLiquidated(): boolean {
1844
- return (
2033
+ public isBeingLiquidated(marginCalc?: MarginCalculation): boolean {
2034
+ // Consider on-chain flags OR computed margin status (cross or any isolated)
2035
+ const hasOnChainFlag =
1845
2036
  (this.getUserAccount().status &
1846
2037
  (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
1847
- 0
2038
+ 0;
2039
+ const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
2040
+ return (
2041
+ hasOnChainFlag ||
2042
+ this.isCrossMarginBeingLiquidated(calc) ||
2043
+ this.isIsolatedMarginBeingLiquidated(calc)
1848
2044
  );
1849
2045
  }
1850
2046
 
2047
+ /** Returns true if cross margin is currently below maintenance requirement (no buffer). */
2048
+ public isCrossMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean {
2049
+ const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
2050
+ return calc.totalCollateral.lt(calc.marginRequirement);
2051
+ }
2052
+
2053
+ /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */
2054
+ public isIsolatedMarginBeingLiquidated(
2055
+ marginCalc?: MarginCalculation
2056
+ ): boolean {
2057
+ const calc = marginCalc ?? this.getMarginCalculation('Maintenance');
2058
+ for (const [, isoCalc] of calc.isolatedMarginCalculations) {
2059
+ if (isoCalc.totalCollateral.lt(isoCalc.marginRequirement)) {
2060
+ return true;
2061
+ }
2062
+ }
2063
+ return false;
2064
+ }
2065
+
1851
2066
  public hasStatus(status: UserStatus): boolean {
1852
2067
  return (this.getUserAccount().status & status) > 0;
1853
2068
  }
@@ -2004,8 +2219,61 @@ export class User {
2004
2219
  marginCategory: MarginCategory = 'Maintenance',
2005
2220
  includeOpenOrders = false,
2006
2221
  offsetCollateral = ZERO,
2007
- enteringHighLeverage = undefined
2222
+ enteringHighLeverage = false,
2223
+ marginType?: MarginType
2008
2224
  ): BN {
2225
+ const market = this.driftClient.getPerpMarketAccount(marketIndex);
2226
+
2227
+ const oracle =
2228
+ this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle;
2229
+
2230
+ const oraclePrice =
2231
+ this.driftClient.getOracleDataForPerpMarket(marketIndex).price;
2232
+
2233
+ const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex);
2234
+
2235
+ if (marginType === 'Isolated') {
2236
+ const marginCalculation = this.getMarginCalculation(marginCategory, {
2237
+ strict: false,
2238
+ includeOpenOrders,
2239
+ enteringHighLeverage,
2240
+ });
2241
+ const isolatedMarginCalculation =
2242
+ marginCalculation.isolatedMarginCalculations.get(marketIndex);
2243
+ const { totalCollateral, marginRequirement } = isolatedMarginCalculation;
2244
+
2245
+ const freeCollateral = BN.max(
2246
+ ZERO,
2247
+ totalCollateral.sub(marginRequirement)
2248
+ ).add(offsetCollateral);
2249
+
2250
+ const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp(
2251
+ market,
2252
+ currentPerpPosition,
2253
+ positionBaseSizeChange,
2254
+ oraclePrice,
2255
+ marginCategory,
2256
+ includeOpenOrders,
2257
+ enteringHighLeverage
2258
+ );
2259
+
2260
+ if (freeCollateralDelta.eq(ZERO)) {
2261
+ return new BN(-1);
2262
+ }
2263
+
2264
+ const liqPriceDelta = freeCollateral
2265
+ .mul(QUOTE_PRECISION)
2266
+ .div(freeCollateralDelta);
2267
+
2268
+ const liqPrice = oraclePrice.sub(liqPriceDelta);
2269
+
2270
+ if (liqPrice.lt(ZERO)) {
2271
+ return new BN(-1);
2272
+ }
2273
+
2274
+ return liqPrice;
2275
+ }
2276
+
2009
2277
  const totalCollateral = this.getTotalCollateral(
2010
2278
  marginCategory,
2011
2279
  false,
@@ -2023,15 +2291,6 @@ export class User {
2023
2291
  totalCollateral.sub(marginRequirement)
2024
2292
  ).add(offsetCollateral);
2025
2293
 
2026
- const oracle =
2027
- this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle;
2028
-
2029
- const oraclePrice =
2030
- this.driftClient.getOracleDataForPerpMarket(marketIndex).price;
2031
-
2032
- const market = this.driftClient.getPerpMarketAccount(marketIndex);
2033
- const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex);
2034
-
2035
2294
  positionBaseSizeChange = standardizeBaseAssetAmount(
2036
2295
  positionBaseSizeChange,
2037
2296
  market.amm.orderStepSize
@@ -3913,4 +4172,287 @@ export class User {
3913
4172
  activeSpotPositions: activeSpotMarkets,
3914
4173
  };
3915
4174
  }
4175
+
4176
+ /**
4177
+ * Compute a consolidated margin snapshot once, without caching.
4178
+ * Consumers can use this to avoid duplicating work across separate calls.
4179
+ */
4180
+ // TODO: need another param to tell it give it back leverage compnents
4181
+ // TODO: change get leverage functions need to pull the right values from
4182
+ public getMarginCalculation(
4183
+ marginCategory: MarginCategory = 'Initial',
4184
+ opts?: {
4185
+ strict?: boolean; // mirror StrictOraclePrice application
4186
+ includeOpenOrders?: boolean;
4187
+ enteringHighLeverage?: boolean;
4188
+ liquidationBuffer?: BN; // margin_buffer analog for buffer mode
4189
+ marginRatioOverride?: number; // mirrors context.margin_ratio_override
4190
+ }
4191
+ ): MarginCalculation {
4192
+ const strict = opts?.strict ?? false;
4193
+ const enteringHighLeverage = opts?.enteringHighLeverage ?? false;
4194
+ const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ??
4195
+ const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided
4196
+ const marginRatioOverride = opts?.marginRatioOverride;
4197
+
4198
+ // Equivalent to on-chain user_custom_margin_ratio
4199
+ let userCustomMarginRatio =
4200
+ marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0;
4201
+ if (marginRatioOverride !== undefined) {
4202
+ userCustomMarginRatio = Math.max(
4203
+ userCustomMarginRatio,
4204
+ marginRatioOverride
4205
+ );
4206
+ }
4207
+
4208
+ // Initialize calc via JS mirror of Rust MarginCalculation
4209
+ const ctx = MarginContext.standard(marginCategory)
4210
+ .strictMode(strict)
4211
+ .setMarginBuffer(marginBuffer)
4212
+ .setMarginRatioOverride(userCustomMarginRatio);
4213
+ const calc = new MarginCalculation(ctx);
4214
+
4215
+ // SPOT POSITIONS
4216
+ // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions
4217
+ for (const spotPosition of this.getUserAccount().spotPositions) {
4218
+ if (isSpotPositionAvailable(spotPosition)) continue;
4219
+
4220
+ const spotMarket = this.driftClient.getSpotMarketAccount(
4221
+ spotPosition.marketIndex
4222
+ );
4223
+ const oraclePriceData = this.getOracleDataForSpotMarket(
4224
+ spotPosition.marketIndex
4225
+ );
4226
+ const twap5 = strict
4227
+ ? calculateLiveOracleTwap(
4228
+ spotMarket.historicalOracleData,
4229
+ oraclePriceData,
4230
+ new BN(Math.floor(Date.now() / 1000)),
4231
+ FIVE_MINUTE
4232
+ )
4233
+ : undefined;
4234
+ const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5);
4235
+
4236
+ if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) {
4237
+ const tokenAmount = getSignedTokenAmount(
4238
+ getTokenAmount(
4239
+ spotPosition.scaledBalance,
4240
+ spotMarket,
4241
+ spotPosition.balanceType
4242
+ ),
4243
+ spotPosition.balanceType
4244
+ );
4245
+ if (isVariant(spotPosition.balanceType, 'deposit')) {
4246
+ // add deposit value to total collateral
4247
+ const tokenValue = getStrictTokenValue(
4248
+ tokenAmount,
4249
+ spotMarket.decimals,
4250
+ strictOracle
4251
+ );
4252
+ calc.addCrossMarginTotalCollateral(tokenValue);
4253
+ } else {
4254
+ // borrow on quote contributes to margin requirement
4255
+ const tokenValueAbs = getStrictTokenValue(
4256
+ tokenAmount,
4257
+ spotMarket.decimals,
4258
+ strictOracle
4259
+ ).abs();
4260
+ calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs);
4261
+ calc.addSpotLiability();
4262
+ }
4263
+ continue;
4264
+ }
4265
+
4266
+ // Non-quote spot: worst-case simulation
4267
+ const {
4268
+ tokenAmount: worstCaseTokenAmount,
4269
+ ordersValue: worstCaseOrdersValue,
4270
+ tokenValue: worstCaseTokenValue,
4271
+ weightedTokenValue: worstCaseWeightedTokenValue,
4272
+ } = getWorstCaseTokenAmounts(
4273
+ spotPosition,
4274
+ spotMarket,
4275
+ strictOracle,
4276
+ marginCategory,
4277
+ userCustomMarginRatio,
4278
+ includeOpenOrders
4279
+ );
4280
+
4281
+ // open order IM
4282
+ calc.addCrossMarginRequirement(
4283
+ new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT),
4284
+ ZERO
4285
+ );
4286
+
4287
+ if (worstCaseTokenAmount.gt(ZERO)) {
4288
+ // asset side increases total collateral (weighted)
4289
+ calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue);
4290
+ } else if (worstCaseTokenAmount.lt(ZERO)) {
4291
+ // liability side increases margin requirement (weighted >= abs(token_value))
4292
+ const liabilityWeighted = worstCaseWeightedTokenValue.abs();
4293
+ calc.addCrossMarginRequirement(
4294
+ liabilityWeighted,
4295
+ worstCaseTokenValue.abs()
4296
+ );
4297
+ calc.addSpotLiability();
4298
+ calc.addSpotLiabilityValue(worstCaseTokenValue.abs());
4299
+ } else if (spotPosition.openOrders !== 0) {
4300
+ calc.addSpotLiability();
4301
+ calc.addSpotLiabilityValue(worstCaseTokenValue.abs());
4302
+ }
4303
+
4304
+ // orders value contributes to collateral or requirement
4305
+ if (worstCaseOrdersValue.gt(ZERO)) {
4306
+ calc.addCrossMarginTotalCollateral(worstCaseOrdersValue);
4307
+ } else if (worstCaseOrdersValue.lt(ZERO)) {
4308
+ const absVal = worstCaseOrdersValue.abs();
4309
+ calc.addCrossMarginRequirement(absVal, absVal);
4310
+ }
4311
+ }
4312
+
4313
+ // PERP POSITIONS
4314
+ for (const marketPosition of this.getActivePerpPositions()) {
4315
+ const market = this.driftClient.getPerpMarketAccount(
4316
+ marketPosition.marketIndex
4317
+ );
4318
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
4319
+ market.quoteSpotMarketIndex
4320
+ );
4321
+ const quoteOraclePriceData = this.getOracleDataForSpotMarket(
4322
+ market.quoteSpotMarketIndex
4323
+ );
4324
+ const oraclePriceData = this.getOracleDataForPerpMarket(
4325
+ market.marketIndex
4326
+ );
4327
+
4328
+ // Worst-case perp liability and weighted pnl
4329
+ const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } =
4330
+ calculateWorstCasePerpLiabilityValue(
4331
+ marketPosition,
4332
+ market,
4333
+ oraclePriceData.price,
4334
+ includeOpenOrders
4335
+ );
4336
+
4337
+ // margin ratio for this perp
4338
+ const customMarginRatio = Math.max(
4339
+ this.getUserAccount().maxMarginRatio,
4340
+ marketPosition.maxMarginRatio
4341
+ );
4342
+ let marginRatio = new BN(
4343
+ calculateMarketMarginRatio(
4344
+ market,
4345
+ worstCaseBaseAssetAmount.abs(),
4346
+ marginCategory,
4347
+ customMarginRatio,
4348
+ this.isHighLeverageMode(marginCategory) || enteringHighLeverage
4349
+ )
4350
+ );
4351
+ if (isVariant(market.status, 'settlement')) {
4352
+ marginRatio = ZERO;
4353
+ }
4354
+
4355
+ // convert liability to quote value and apply margin ratio
4356
+ const quotePrice = strict
4357
+ ? BN.max(
4358
+ quoteOraclePriceData.price,
4359
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4360
+ )
4361
+ : quoteOraclePriceData.price;
4362
+ let perpMarginRequirement = worstCaseLiabilityValue
4363
+ .mul(quotePrice)
4364
+ .div(PRICE_PRECISION)
4365
+ .mul(marginRatio)
4366
+ .div(MARGIN_PRECISION);
4367
+ // add open orders IM
4368
+ perpMarginRequirement = perpMarginRequirement.add(
4369
+ new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
4370
+ );
4371
+
4372
+ // weighted unrealized pnl
4373
+ let positionUnrealizedPnl = calculatePositionPNL(
4374
+ market,
4375
+ marketPosition,
4376
+ true,
4377
+ oraclePriceData
4378
+ );
4379
+ let pnlQuotePrice: BN;
4380
+ if (strict && positionUnrealizedPnl.gt(ZERO)) {
4381
+ pnlQuotePrice = BN.min(
4382
+ quoteOraclePriceData.price,
4383
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4384
+ );
4385
+ } else if (strict && positionUnrealizedPnl.lt(ZERO)) {
4386
+ pnlQuotePrice = BN.max(
4387
+ quoteOraclePriceData.price,
4388
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4389
+ );
4390
+ } else {
4391
+ pnlQuotePrice = quoteOraclePriceData.price;
4392
+ }
4393
+ positionUnrealizedPnl = positionUnrealizedPnl
4394
+ .mul(pnlQuotePrice)
4395
+ .div(PRICE_PRECISION);
4396
+
4397
+ // Add perp contribution: isolated vs cross
4398
+ const isIsolated = this.isPerpPositionIsolated(marketPosition);
4399
+ if (isIsolated) {
4400
+ // derive isolated quote deposit value, mirroring on-chain logic
4401
+ let depositValue = ZERO;
4402
+ if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) {
4403
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
4404
+ market.quoteSpotMarketIndex
4405
+ );
4406
+ const quoteOraclePriceData = this.getOracleDataForSpotMarket(
4407
+ market.quoteSpotMarketIndex
4408
+ );
4409
+ const strictQuote = new StrictOraclePrice(
4410
+ quoteOraclePriceData.price,
4411
+ strict
4412
+ ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
4413
+ : undefined
4414
+ );
4415
+ const quoteTokenAmount = getTokenAmount(
4416
+ marketPosition.isolatedPositionScaledBalance,
4417
+ quoteSpotMarket,
4418
+ SpotBalanceType.DEPOSIT
4419
+ );
4420
+ depositValue = getStrictTokenValue(
4421
+ quoteTokenAmount,
4422
+ quoteSpotMarket.decimals,
4423
+ strictQuote
4424
+ );
4425
+ }
4426
+ calc.addIsolatedMarginCalculation(
4427
+ market.marketIndex,
4428
+ depositValue,
4429
+ positionUnrealizedPnl,
4430
+ worstCaseLiabilityValue,
4431
+ perpMarginRequirement
4432
+ );
4433
+ calc.addPerpLiability();
4434
+ calc.addPerpLiabilityValue(worstCaseLiabilityValue);
4435
+ } else {
4436
+ // cross: add to global requirement and collateral
4437
+ calc.addCrossMarginRequirement(
4438
+ perpMarginRequirement,
4439
+ worstCaseLiabilityValue
4440
+ );
4441
+ calc.addCrossMarginTotalCollateral(positionUnrealizedPnl);
4442
+ const hasPerpLiability =
4443
+ !marketPosition.baseAssetAmount.eq(ZERO) ||
4444
+ marketPosition.quoteAssetAmount.lt(ZERO) ||
4445
+ marketPosition.openOrders !== 0;
4446
+ if (hasPerpLiability) {
4447
+ calc.addPerpLiability();
4448
+ }
4449
+ }
4450
+ }
4451
+
4452
+ return calc;
4453
+ }
4454
+
4455
+ private isPerpPositionIsolated(perpPosition: PerpPosition): boolean {
4456
+ return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0;
4457
+ }
3916
4458
  }