@drift-labs/sdk-browser 2.155.0-beta.3 → 2.155.0-beta.5

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 (57) hide show
  1. package/VERSION +1 -1
  2. package/lib/browser/decode/user.js +8 -5
  3. package/lib/browser/driftClient.d.ts +15 -10
  4. package/lib/browser/driftClient.js +137 -23
  5. package/lib/browser/marginCalculation.d.ts +0 -12
  6. package/lib/browser/marginCalculation.js +0 -20
  7. package/lib/browser/math/margin.js +1 -0
  8. package/lib/browser/math/position.d.ts +1 -0
  9. package/lib/browser/math/position.js +10 -2
  10. package/lib/browser/swap/UnifiedSwapClient.js +1 -10
  11. package/lib/browser/titan/titanClient.d.ts +4 -5
  12. package/lib/browser/titan/titanClient.js +2 -16
  13. package/lib/browser/types.d.ts +9 -6
  14. package/lib/browser/types.js +11 -7
  15. package/lib/browser/user.js +13 -7
  16. package/lib/node/decode/user.d.ts.map +1 -1
  17. package/lib/node/decode/user.js +8 -5
  18. package/lib/node/driftClient.d.ts +15 -10
  19. package/lib/node/driftClient.d.ts.map +1 -1
  20. package/lib/node/driftClient.js +137 -23
  21. package/lib/node/marginCalculation.d.ts +0 -12
  22. package/lib/node/marginCalculation.d.ts.map +1 -1
  23. package/lib/node/marginCalculation.js +0 -20
  24. package/lib/node/math/margin.d.ts.map +1 -1
  25. package/lib/node/math/margin.js +1 -0
  26. package/lib/node/math/position.d.ts +1 -0
  27. package/lib/node/math/position.d.ts.map +1 -1
  28. package/lib/node/math/position.js +10 -2
  29. package/lib/node/math/spotBalance.d.ts.map +1 -1
  30. package/lib/node/swap/UnifiedSwapClient.d.ts.map +1 -1
  31. package/lib/node/swap/UnifiedSwapClient.js +1 -10
  32. package/lib/node/titan/titanClient.d.ts +4 -5
  33. package/lib/node/titan/titanClient.d.ts.map +1 -1
  34. package/lib/node/titan/titanClient.js +2 -16
  35. package/lib/node/types.d.ts +9 -6
  36. package/lib/node/types.d.ts.map +1 -1
  37. package/lib/node/types.js +11 -7
  38. package/lib/node/user.d.ts.map +1 -1
  39. package/lib/node/user.js +13 -7
  40. package/package.json +1 -1
  41. package/scripts/deposit-isolated-positions.ts +110 -0
  42. package/scripts/find-flagged-users.ts +216 -0
  43. package/scripts/single-grpc-client-test.ts +71 -21
  44. package/scripts/withdraw-isolated-positions.ts +174 -0
  45. package/src/decode/user.ts +14 -6
  46. package/src/driftClient.ts +297 -65
  47. package/src/margin/README.md +139 -0
  48. package/src/marginCalculation.ts +0 -32
  49. package/src/math/margin.ts +2 -3
  50. package/src/math/position.ts +12 -2
  51. package/src/math/spotBalance.ts +0 -1
  52. package/src/swap/UnifiedSwapClient.ts +2 -13
  53. package/src/titan/titanClient.ts +4 -28
  54. package/src/types.ts +11 -7
  55. package/src/user.ts +17 -8
  56. package/tests/dlob/helpers.ts +1 -1
  57. package/tests/user/test.ts +1 -1
@@ -0,0 +1,139 @@
1
+ ## Margin Calculation Snapshot (SDK)
2
+
3
+ This document describes the single-source-of-truth margin engine in the SDK that mirrors the on-chain `MarginCalculation` and related semantics. The goal is to compute an immutable snapshot in one pass and have existing `User` getters delegate to it, eliminating duplicative work across getters and UI hooks while maintaining parity with the program.
4
+
5
+ ### Alignment with on-chain
6
+
7
+ - The SDK snapshot shape mirrors `programs/drift/src/state/margin_calculation.rs` field-for-field.
8
+ - The inputs and ordering mirror `calculate_margin_requirement_and_total_collateral_and_liability_info` in `programs/drift/src/math/margin.rs`.
9
+ - Isolated positions are represented as `isolatedMarginCalculations` keyed by perp `marketIndex`, matching program logic.
10
+
11
+ ### Core SDK types (shape parity)
12
+
13
+ ```ts
14
+ // Types reflect on-chain names and numeric signs
15
+ import { MarketType } from './types';
16
+
17
+ export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill';
18
+
19
+ export type MarginCalculationMode =
20
+ | { type: 'Standard' }
21
+ | { type: 'Liquidation' };
22
+
23
+ export class MarketIdentifier {
24
+ marketType: MarketType;
25
+ marketIndex: number;
26
+
27
+ static spot(marketIndex: number): MarketIdentifier;
28
+ static perp(marketIndex: number): MarketIdentifier;
29
+ equals(other: MarketIdentifier | undefined): boolean;
30
+ }
31
+
32
+ export class MarginContext {
33
+ marginType: MarginCategory;
34
+ mode: MarginCalculationMode;
35
+ strict: boolean;
36
+ ignoreInvalidDepositOracles: boolean;
37
+ isolatedMarginBuffers: Map<number, BN>;
38
+ crossMarginBuffer: BN;
39
+
40
+ // Factory methods
41
+ static standard(marginType: MarginCategory): MarginContext;
42
+ static liquidation(
43
+ crossMarginBuffer: BN,
44
+ isolatedMarginBuffers: Map<number, BN>
45
+ ): MarginContext;
46
+
47
+ // Builder methods (return this for chaining)
48
+ strictMode(strict: boolean): this;
49
+ ignoreInvalidDeposits(ignore: boolean): this;
50
+ setCrossMarginBuffer(crossMarginBuffer: BN): this;
51
+ setIsolatedMarginBuffers(isolatedMarginBuffers: Map<number, BN>): this;
52
+ setIsolatedMarginBuffer(marketIndex: number, isolatedMarginBuffer: BN): this;
53
+ }
54
+
55
+ export class IsolatedMarginCalculation {
56
+ marginRequirement: BN; // u128
57
+ totalCollateral: BN; // i128 (deposit + pnl)
58
+ totalCollateralBuffer: BN; // i128
59
+ marginRequirementPlusBuffer: BN; // u128
60
+
61
+ getTotalCollateralPlusBuffer(): BN;
62
+ meetsMarginRequirement(): boolean;
63
+ meetsMarginRequirementWithBuffer(): boolean;
64
+ marginShortage(): BN;
65
+ }
66
+
67
+ export class MarginCalculation {
68
+ context: MarginContext;
69
+
70
+ totalCollateral: BN; // i128
71
+ totalCollateralBuffer: BN; // i128
72
+ marginRequirement: BN; // u128
73
+ marginRequirementPlusBuffer: BN; // u128
74
+
75
+ isolatedMarginCalculations: Map<number, IsolatedMarginCalculation>;
76
+
77
+ totalPerpLiabilityValue: BN; // u128
78
+
79
+ // Cross margin helpers
80
+ getCrossTotalCollateralPlusBuffer(): BN;
81
+ meetsCrossMarginRequirement(): boolean;
82
+ meetsCrossMarginRequirementWithBuffer(): boolean;
83
+ getCrossFreeCollateral(): BN;
84
+
85
+ // Combined (cross + isolated) helpers
86
+ meetsMarginRequirement(): boolean;
87
+ meetsMarginRequirementWithBuffer(): boolean;
88
+
89
+ // Isolated margin helpers
90
+ getIsolatedFreeCollateral(marketIndex: number): BN;
91
+ getIsolatedMarginCalculation(marketIndex: number): IsolatedMarginCalculation | undefined;
92
+ hasIsolatedMarginCalculation(marketIndex: number): boolean;
93
+ }
94
+ ```
95
+
96
+ ### Computation model (on-demand)
97
+
98
+ - The SDK computes the snapshot on-demand when `getMarginCalculation(...)` is called.
99
+ - No event-driven recomputation by default (oracle prices can change every slot; recomputing every update would be wasteful).
100
+ - Callers (UI/bots) decide polling frequency (e.g., UI can refresh every ~1s on active trade forms).
101
+
102
+ ### User integration
103
+
104
+ `User` class provides the primary entrypoint:
105
+
106
+ ```ts
107
+ public getMarginCalculation(
108
+ marginCategory: MarginCategory = 'Initial',
109
+ opts?: {
110
+ strict?: boolean; // mirror StrictOraclePrice application
111
+ includeOpenOrders?: boolean; // include open orders in margin calc
112
+ enteringHighLeverage?: boolean; // entering high leverage mode
113
+ liquidationBufferMap?: Map<number | 'cross', BN>; // margin buffer for liquidation mode
114
+ }
115
+ ): MarginCalculation;
116
+ ```
117
+
118
+ Existing getters delegate to the snapshot to avoid duplicate work:
119
+ - `getTotalCollateral()` → `snapshot.totalCollateral`
120
+ - `getMarginRequirement(mode)` → `snapshot.marginRequirement`
121
+ - `getFreeCollateral()` → `snapshot.getCrossFreeCollateral()`
122
+ - Per-market isolated FC → `snapshot.getIsolatedFreeCollateral(marketIndex)`
123
+
124
+ ### UI compatibility
125
+
126
+ - All existing `User` getters remain and delegate to the snapshot, so current UI keeps working without call-site changes.
127
+ - New consumers can call `user.getMarginCalculation()` to access isolated breakdowns via `isolatedMarginCalculations`.
128
+
129
+ ### Testing and parity
130
+
131
+ - Golden tests comparing SDK snapshot against program outputs (cross and isolated, edge cases).
132
+ - Keep math/rounding identical to program (ordering, buffers, funding, open-order IM, oracle strictness).
133
+
134
+ ### Migration plan (brief)
135
+
136
+ 1. Implement `types` and `engine` with strict parity; land behind a feature flag.
137
+ 2. Add `user.getMarginCalculation()` and delegate legacy getters.
138
+ 3. Optionally update UI hooks to read richer fields; not required for compatibility.
139
+ 4. Expand parity tests; enable by default after validation.
@@ -131,15 +131,7 @@ export class MarginCalculation {
131
131
  marginRequirement: BN;
132
132
  marginRequirementPlusBuffer: BN;
133
133
  isolatedMarginCalculations: Map<number, IsolatedMarginCalculation>;
134
- allDepositOraclesValid: boolean;
135
- allLiabilityOraclesValid: boolean;
136
- withPerpIsolatedLiability: boolean;
137
- withSpotIsolatedLiability: boolean;
138
134
  totalPerpLiabilityValue: BN;
139
- trackedMarketMarginRequirement: BN;
140
- fuelDeposits: number;
141
- fuelBorrows: number;
142
- fuelPositions: number;
143
135
 
144
136
  constructor(context: MarginContext) {
145
137
  this.context = context;
@@ -148,15 +140,7 @@ export class MarginCalculation {
148
140
  this.marginRequirement = ZERO;
149
141
  this.marginRequirementPlusBuffer = ZERO;
150
142
  this.isolatedMarginCalculations = new Map();
151
- this.allDepositOraclesValid = true;
152
- this.allLiabilityOraclesValid = true;
153
- this.withPerpIsolatedLiability = false;
154
- this.withSpotIsolatedLiability = false;
155
143
  this.totalPerpLiabilityValue = ZERO;
156
- this.trackedMarketMarginRequirement = ZERO;
157
- this.fuelDeposits = 0;
158
- this.fuelBorrows = 0;
159
- this.fuelPositions = 0;
160
144
  }
161
145
 
162
146
  addCrossMarginTotalCollateral(delta: BN): void {
@@ -216,22 +200,6 @@ export class MarginCalculation {
216
200
  this.totalPerpLiabilityValue.add(perpLiabilityValue);
217
201
  }
218
202
 
219
- updateAllDepositOraclesValid(valid: boolean): void {
220
- this.allDepositOraclesValid = this.allDepositOraclesValid && valid;
221
- }
222
-
223
- updateAllLiabilityOraclesValid(valid: boolean): void {
224
- this.allLiabilityOraclesValid = this.allLiabilityOraclesValid && valid;
225
- }
226
-
227
- updateWithSpotIsolatedLiability(isolated: boolean): void {
228
- this.withSpotIsolatedLiability = this.withSpotIsolatedLiability || isolated;
229
- }
230
-
231
- updateWithPerpIsolatedLiability(isolated: boolean): void {
232
- this.withPerpIsolatedLiability = this.withPerpIsolatedLiability || isolated;
233
- }
234
-
235
203
  getCrossTotalCollateralPlusBuffer(): BN {
236
204
  return this.totalCollateral.add(this.totalCollateralBuffer);
237
205
  }
@@ -166,10 +166,10 @@ export function calculateWorstCasePerpLiabilityValue(
166
166
  perpPosition: PerpPosition,
167
167
  perpMarket: PerpMarketAccount,
168
168
  oraclePrice: BN,
169
- includeOpenOrders = true
169
+ includeOpenOrders: boolean = true
170
170
  ): { worstCaseBaseAssetAmount: BN; worstCaseLiabilityValue: BN } {
171
171
  const isPredictionMarket = isVariant(perpMarket.contractType, 'prediction');
172
-
172
+ // return early if no open orders required
173
173
  if (!includeOpenOrders) {
174
174
  return {
175
175
  worstCaseBaseAssetAmount: perpPosition.baseAssetAmount,
@@ -180,7 +180,6 @@ export function calculateWorstCasePerpLiabilityValue(
180
180
  ),
181
181
  };
182
182
  }
183
-
184
183
  const allBids = perpPosition.baseAssetAmount.add(perpPosition.openBids);
185
184
  const allAsks = perpPosition.baseAssetAmount.add(perpPosition.openAsks);
186
185
 
@@ -14,6 +14,7 @@ import {
14
14
  PositionDirection,
15
15
  PerpPosition,
16
16
  SpotMarketAccount,
17
+ PositionFlag,
17
18
  } from '../types';
18
19
  import {
19
20
  calculateUpdatedAMM,
@@ -127,7 +128,6 @@ export function calculatePositionPNL(
127
128
 
128
129
  if (withFunding) {
129
130
  const fundingRatePnL = calculateUnsettledFundingPnl(market, perpPosition);
130
-
131
131
  pnl = pnl.add(fundingRatePnL);
132
132
  }
133
133
 
@@ -244,7 +244,17 @@ export function positionIsAvailable(position: PerpPosition): boolean {
244
244
  position.baseAssetAmount.eq(ZERO) &&
245
245
  position.openOrders === 0 &&
246
246
  position.quoteAssetAmount.eq(ZERO) &&
247
- position.lpShares.eq(ZERO)
247
+ position.lpShares.eq(ZERO) &&
248
+ position.isolatedPositionScaledBalance.eq(ZERO) &&
249
+ !positionIsBeingLiquidated(position)
250
+ );
251
+ }
252
+
253
+ export function positionIsBeingLiquidated(position: PerpPosition): boolean {
254
+ return (
255
+ (position.positionFlag &
256
+ (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) >
257
+ 0
248
258
  );
249
259
  }
250
260
 
@@ -68,7 +68,6 @@ export function getTokenAmount(
68
68
  balanceType: SpotBalanceType
69
69
  ): BN {
70
70
  const precisionDecrease = TEN.pow(new BN(19 - spotMarket.decimals));
71
-
72
71
  if (isVariant(balanceType, 'deposit')) {
73
72
  return balanceAmount
74
73
  .mul(spotMarket.cumulativeDepositInterest)
@@ -11,11 +11,7 @@ import {
11
11
  JupiterClient,
12
12
  QuoteResponse as JupiterQuoteResponse,
13
13
  } from '../jupiter/jupiterClient';
14
- import {
15
- TitanClient,
16
- QuoteResponse as TitanQuoteResponse,
17
- SwapMode as TitanSwapMode,
18
- } from '../titan/titanClient';
14
+ import { TitanClient, SwapMode as TitanSwapMode } from '../titan/titanClient';
19
15
 
20
16
  export type SwapMode = 'ExactIn' | 'ExactOut';
21
17
  export type SwapClientType = 'jupiter' | 'titan';
@@ -167,18 +163,11 @@ export class UnifiedSwapClient {
167
163
  return { transaction };
168
164
  } else {
169
165
  const titanClient = this.client as TitanClient;
170
- const { quote, userPublicKey, slippageBps } = params;
166
+ const { userPublicKey } = params;
171
167
 
172
168
  // For Titan, we need to reconstruct the parameters from the quote
173
- const titanQuote = quote as TitanQuoteResponse;
174
169
  const result = await titanClient.getSwap({
175
- inputMint: new PublicKey(titanQuote.inputMint),
176
- outputMint: new PublicKey(titanQuote.outputMint),
177
- amount: new BN(titanQuote.inAmount),
178
170
  userPublicKey,
179
- slippageBps: slippageBps || titanQuote.slippageBps,
180
- swapMode: titanQuote.swapMode,
181
- sizeConstraint: 1280 - 375, // MAX_TX_BYTE_SIZE - buffer for drift instructions
182
171
  });
183
172
 
184
173
  return {
@@ -287,21 +287,11 @@ export class TitanClient {
287
287
  * Get a swap transaction for quote
288
288
  */
289
289
  public async getSwap({
290
- inputMint,
291
- outputMint,
292
- amount,
293
290
  userPublicKey,
294
- maxAccounts = 50, // 50 is an estimated amount with buffer
295
- slippageBps,
296
- swapMode,
297
- onlyDirectRoutes,
298
- excludeDexes,
299
- sizeConstraint,
300
- accountsLimitWritable,
301
291
  }: {
302
- inputMint: PublicKey;
303
- outputMint: PublicKey;
304
- amount: BN;
292
+ inputMint?: PublicKey;
293
+ outputMint?: PublicKey;
294
+ amount?: BN;
305
295
  userPublicKey: PublicKey;
306
296
  maxAccounts?: number;
307
297
  slippageBps?: number;
@@ -314,22 +304,8 @@ export class TitanClient {
314
304
  transactionMessage: TransactionMessage;
315
305
  lookupTables: AddressLookupTableAccount[];
316
306
  }> {
317
- const params = this.buildParams({
318
- inputMint,
319
- outputMint,
320
- amount,
321
- userPublicKey,
322
- maxAccounts,
323
- slippageBps,
324
- swapMode,
325
- onlyDirectRoutes,
326
- excludeDexes,
327
- sizeConstraint,
328
- accountsLimitWritable,
329
- });
330
-
331
307
  // Check if we have cached quote data that matches the current parameters
332
- if (!this.lastQuoteData || this.lastQuoteParams !== params.toString()) {
308
+ if (!this.lastQuoteData) {
333
309
  throw new Error(
334
310
  'No matching quote data found. Please get a fresh quote before attempting to swap.'
335
311
  );
package/src/types.ts CHANGED
@@ -591,6 +591,10 @@ export type SpotBankruptcyRecord = {
591
591
  ifPayment: BN;
592
592
  };
593
593
 
594
+ export class LiquidationBitFlag {
595
+ static readonly IsolatedPosition = 1;
596
+ }
597
+
594
598
  export type SettlePnlRecord = {
595
599
  ts: BN;
596
600
  user: PublicKey;
@@ -1142,16 +1146,10 @@ export type PerpPosition = {
1142
1146
  maxMarginRatio: number;
1143
1147
  lastQuoteAssetAmountPerLp: BN;
1144
1148
  perLpBase: number;
1145
- isolatedPositionScaledBalance: BN;
1146
1149
  positionFlag: number;
1150
+ isolatedPositionScaledBalance: BN;
1147
1151
  };
1148
1152
 
1149
- export class PositionFlag {
1150
- static readonly IsolatedPosition = 1;
1151
- static readonly BeingLiquidated = 2;
1152
- static readonly Bankruptcy = 4;
1153
- }
1154
-
1155
1153
  export type UserStatsAccount = {
1156
1154
  numberOfSubAccounts: number;
1157
1155
  numberOfSubAccountsCreated: number;
@@ -1302,6 +1300,12 @@ export class OrderParamsBitFlag {
1302
1300
  static readonly UpdateHighLeverageMode = 2;
1303
1301
  }
1304
1302
 
1303
+ export class PositionFlag {
1304
+ static readonly IsolatedPosition = 1;
1305
+ static readonly BeingLiquidated = 2;
1306
+ static readonly Bankruptcy = 4;
1307
+ }
1308
+
1305
1309
  export type NecessaryOrderParams = {
1306
1310
  orderType: OrderType;
1307
1311
  marketIndex: number;
package/src/user.ts CHANGED
@@ -504,8 +504,9 @@ export class User {
504
504
  )
505
505
  : ZERO;
506
506
 
507
- let freeCollateral: BN;
508
- if (positionType === 'isolated' && this.isPositionEmpty(perpPosition)) {
507
+ let freeCollateral: BN = ZERO;
508
+ // if position is isolated, we always add on available quote from the cross account
509
+ if (positionType === 'isolated') {
509
510
  const {
510
511
  totalAssetValue: quoteSpotMarketAssetValue,
511
512
  totalLiabilityValue: quoteSpotMarketLiabilityValue,
@@ -520,13 +521,16 @@ export class User {
520
521
  freeCollateral = quoteSpotMarketAssetValue.sub(
521
522
  quoteSpotMarketLiabilityValue
522
523
  );
523
- } else {
524
- freeCollateral = this.getFreeCollateral(
524
+ }
525
+
526
+ // adding free collateral from the cross account or from within isolated margin calc for this marketIndex
527
+ freeCollateral = freeCollateral.add(
528
+ this.getFreeCollateral(
525
529
  'Initial',
526
530
  enterHighLeverageMode,
527
531
  positionType === 'isolated' ? marketIndex : undefined
528
- ).sub(collateralBuffer);
529
- }
532
+ ).sub(collateralBuffer)
533
+ );
530
534
 
531
535
  return this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(
532
536
  marketIndex,
@@ -574,7 +578,12 @@ export class User {
574
578
  });
575
579
 
576
580
  if (perpMarketIndex !== undefined) {
577
- return calc.getIsolatedFreeCollateral(perpMarketIndex);
581
+ // getIsolatedFreeCollateral will throw if no existing isolated position but we are fetching for potential new position, so we wrap in a try/catch
582
+ try {
583
+ return calc.getIsolatedFreeCollateral(perpMarketIndex);
584
+ } catch (error) {
585
+ return ZERO;
586
+ }
578
587
  } else {
579
588
  return calc.getCrossFreeCollateral();
580
589
  }
@@ -2363,7 +2372,7 @@ export class User {
2363
2372
  enteringHighLeverage
2364
2373
  );
2365
2374
 
2366
- if (freeCollateralDelta.eq(ZERO)) {
2375
+ if (!freeCollateralDelta || freeCollateralDelta.eq(ZERO)) {
2367
2376
  return new BN(-1);
2368
2377
  }
2369
2378
 
@@ -781,4 +781,4 @@ export class MockUserMap implements UserMapInterface {
781
781
  ];
782
782
  }
783
783
  }
784
- }
784
+ }
@@ -49,7 +49,7 @@ async function makeMockUser(
49
49
  oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] =
50
50
  spotOraclePriceList[i];
51
51
  }
52
- // console.log(oraclePriceMap);
52
+ // console.log('oraclePriceMap:', oraclePriceMap);
53
53
 
54
54
  function getMockUserAccount(): UserAccount {
55
55
  return myMockUserAccount;