@quartz-labs/sdk 0.0.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.
@@ -0,0 +1,1056 @@
1
+ import { AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_EXP, BN, calculateAssetWeight, calculateLiabilityWeight, calculateLiveOracleTwap, calculateMarketMarginRatio, calculateMarketOpenBidAsk, calculatePerpLiabilityValue, calculatePositionPNL, calculateUnrealizedAssetWeight, calculateUnsettledFundingPnl, calculateWithdrawLimit, calculateWorstCasePerpLiabilityValue, DriftClient, fetchUserAccountsUsingKeys, FIVE_MINUTE, getSignedTokenAmount, getStrictTokenValue, getTokenAmount, getWorstCaseTokenAmounts, isSpotPositionAvailable, isVariant, MARGIN_PRECISION, MarginCategory, ONE, OPEN_ORDER_MARGIN_REQUIREMENT, PerpPosition, PRICE_PRECISION, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, SPOT_MARKET_WEIGHT_PRECISION, SpotBalanceType, StrictOraclePrice, UserAccount, UserStatus, ZERO, TEN, divCeil, SpotMarketAccount } from "@drift-labs/sdk";
2
+ import { Connection, PublicKey } from "@solana/web3.js";
3
+ import { getDriftUser } from "../helpers.js";
4
+
5
+ export class DriftUser {
6
+ private isInitialized: boolean = false;
7
+ private authority: PublicKey;
8
+ private connection: Connection;
9
+ private driftClient: DriftClient;
10
+
11
+ private userAccount: UserAccount | undefined;
12
+
13
+ constructor (
14
+ authority: PublicKey,
15
+ connection: Connection,
16
+ driftClient: DriftClient,
17
+ userAccount?: UserAccount
18
+ ) {
19
+ this.authority = authority;
20
+ this.connection = connection;
21
+ this.driftClient = driftClient;
22
+
23
+ if (userAccount) {
24
+ this.userAccount = userAccount;
25
+ this.isInitialized = true;
26
+ }
27
+ }
28
+
29
+ public async initialize(): Promise<void> {
30
+ if (this.isInitialized) return;
31
+
32
+ const [ userAccount ] = await fetchUserAccountsUsingKeys(
33
+ this.connection,
34
+ this.driftClient.program,
35
+ [getDriftUser(this.authority)]
36
+ );
37
+ if (!userAccount) throw new Error("Drift user not found");
38
+ this.userAccount = userAccount;
39
+ this.isInitialized = true;
40
+ }
41
+
42
+ public getHealth(): number{
43
+ if (!this.isInitialized) throw new Error("DriftUser not initialized");
44
+
45
+ if (this.isBeingLiquidated()) return 0;
46
+
47
+ const totalCollateral = this.getTotalCollateral('Maintenance');
48
+ const maintenanceMarginReq = this.getMaintenanceMarginRequirement();
49
+
50
+ if (maintenanceMarginReq.eq(ZERO) && totalCollateral.gte(ZERO)) {
51
+ return 100;
52
+ }
53
+
54
+ if (totalCollateral.lte(ZERO)) {
55
+ return 0;
56
+ }
57
+
58
+ return Math.round(
59
+ Math.min(
60
+ 100,
61
+ Math.max(
62
+ 0,
63
+ (1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) * 100
64
+ )
65
+ )
66
+ );
67
+ }
68
+
69
+ public getTokenAmount(marketIndex: number): BN {
70
+ if (!this.isInitialized) throw new Error("DriftUser not initialized");
71
+
72
+ const spotPosition = this.userAccount!.spotPositions.find(
73
+ (position) => position.marketIndex === marketIndex
74
+ );
75
+
76
+ if (spotPosition === undefined) {
77
+ return ZERO;
78
+ }
79
+
80
+ const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex)!;
81
+ return getSignedTokenAmount(
82
+ getTokenAmount(
83
+ spotPosition.scaledBalance,
84
+ spotMarket,
85
+ spotPosition.balanceType
86
+ ),
87
+ spotPosition.balanceType
88
+ );
89
+ }
90
+
91
+ public getWithdrawalLimit(marketIndex: number, reduceOnly?: boolean): BN {
92
+ const nowTs = new BN(Math.floor(Date.now() / 1000));
93
+ const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
94
+
95
+ // eslint-disable-next-line prefer-const
96
+ let { borrowLimit, withdrawLimit } = calculateWithdrawLimit(
97
+ spotMarket!,
98
+ nowTs
99
+ );
100
+
101
+ const freeCollateral = this.getFreeCollateral();
102
+ const initialMarginRequirement = this.getMarginRequirement('Initial', undefined, false);
103
+ const oracleData = this.driftClient.getOracleDataForSpotMarket(marketIndex);
104
+ const precisionIncrease = TEN.pow(new BN(spotMarket!.decimals - 6));
105
+
106
+ const { canBypass, depositAmount: userDepositAmount } =
107
+ this.canBypassWithdrawLimits(marketIndex);
108
+ if (canBypass) {
109
+ withdrawLimit = BN.max(withdrawLimit, userDepositAmount);
110
+ }
111
+
112
+ const assetWeight = calculateAssetWeight(
113
+ userDepositAmount,
114
+ oracleData.price,
115
+ spotMarket!,
116
+ 'Initial'
117
+ );
118
+
119
+ let amountWithdrawable;
120
+ if (assetWeight.eq(ZERO)) {
121
+ amountWithdrawable = userDepositAmount;
122
+ } else if (initialMarginRequirement.eq(ZERO)) {
123
+ amountWithdrawable = userDepositAmount;
124
+ } else {
125
+ amountWithdrawable = divCeil(
126
+ divCeil(freeCollateral.mul(MARGIN_PRECISION), assetWeight).mul(
127
+ PRICE_PRECISION
128
+ ),
129
+ oracleData.price
130
+ ).mul(precisionIncrease);
131
+ }
132
+
133
+ const maxWithdrawValue = BN.min(
134
+ BN.min(amountWithdrawable, userDepositAmount),
135
+ withdrawLimit.abs()
136
+ );
137
+
138
+ if (reduceOnly) {
139
+ return BN.max(maxWithdrawValue, ZERO);
140
+ } else {
141
+ const weightedAssetValue = this.getSpotMarketAssetValue(
142
+ 'Initial',
143
+ marketIndex,
144
+ false
145
+ );
146
+
147
+ const freeCollatAfterWithdraw = userDepositAmount.gt(ZERO)
148
+ ? freeCollateral.sub(weightedAssetValue)
149
+ : freeCollateral;
150
+
151
+ const maxLiabilityAllowed = freeCollatAfterWithdraw
152
+ .mul(MARGIN_PRECISION)
153
+ .div(new BN(spotMarket!.initialLiabilityWeight))
154
+ .mul(PRICE_PRECISION)
155
+ .div(oracleData.price)
156
+ .mul(precisionIncrease);
157
+
158
+ const maxBorrowValue = BN.min(
159
+ maxWithdrawValue.add(maxLiabilityAllowed),
160
+ borrowLimit.abs()
161
+ );
162
+
163
+ return BN.max(maxBorrowValue, ZERO);
164
+ }
165
+ }
166
+
167
+ private getFreeCollateral(marginCategory: MarginCategory = 'Initial'): BN {
168
+ const totalCollateral = this.getTotalCollateral(marginCategory, true);
169
+ const marginRequirement =
170
+ marginCategory === 'Initial'
171
+ ? this.getMarginRequirement('Initial', undefined, false)
172
+ : this.getMaintenanceMarginRequirement();
173
+ const freeCollateral = totalCollateral.sub(marginRequirement);
174
+ return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
175
+ }
176
+
177
+ private canBypassWithdrawLimits(marketIndex: number): {
178
+ canBypass: boolean;
179
+ netDeposits: BN;
180
+ depositAmount: BN;
181
+ maxDepositAmount: BN;
182
+ } {
183
+ const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
184
+ const maxDepositAmount = spotMarket!.withdrawGuardThreshold.div(new BN(10));
185
+ const position = this.userAccount!.spotPositions.find((position) => position.marketIndex === marketIndex);
186
+
187
+ const netDeposits = this.userAccount!.totalDeposits.sub(
188
+ this.userAccount!.totalWithdraws
189
+ );
190
+
191
+ if (!position) {
192
+ return {
193
+ canBypass: false,
194
+ maxDepositAmount,
195
+ depositAmount: ZERO,
196
+ netDeposits,
197
+ };
198
+ }
199
+
200
+ if (isVariant(position.balanceType, 'borrow')) {
201
+ return {
202
+ canBypass: false,
203
+ maxDepositAmount,
204
+ netDeposits,
205
+ depositAmount: ZERO,
206
+ };
207
+ }
208
+
209
+ const depositAmount = getTokenAmount(
210
+ position.scaledBalance,
211
+ spotMarket!,
212
+ SpotBalanceType.DEPOSIT
213
+ );
214
+
215
+ if (netDeposits.lt(ZERO)) {
216
+ return {
217
+ canBypass: false,
218
+ maxDepositAmount,
219
+ depositAmount,
220
+ netDeposits,
221
+ };
222
+ }
223
+
224
+ return {
225
+ canBypass: depositAmount.lt(maxDepositAmount),
226
+ maxDepositAmount,
227
+ netDeposits,
228
+ depositAmount,
229
+ };
230
+ }
231
+
232
+ private isBeingLiquidated(): boolean {
233
+ return (
234
+ (this.userAccount!.status &
235
+ (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
236
+ 0
237
+ );
238
+ }
239
+
240
+ public getTotalCollateral(
241
+ marginCategory: MarginCategory = 'Initial',
242
+ strict = false,
243
+ includeOpenOrders = true
244
+ ): BN {
245
+ if (!this.isInitialized) throw new Error("DriftUser not initialized");
246
+
247
+ return this.getSpotMarketAssetValue(
248
+ marginCategory,
249
+ undefined,
250
+ includeOpenOrders,
251
+ strict
252
+ ).add(this.getUnrealizedPNL(true, undefined, marginCategory, strict));
253
+ }
254
+
255
+ private getSpotMarketAssetValue(
256
+ marginCategory: MarginCategory,
257
+ marketIndex?: number,
258
+ includeOpenOrders?: boolean,
259
+ strict = false,
260
+ now?: BN
261
+ ): BN {
262
+ const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(
263
+ marginCategory,
264
+ marketIndex,
265
+ undefined,
266
+ includeOpenOrders,
267
+ strict,
268
+ now
269
+ );
270
+ return totalAssetValue;
271
+ }
272
+
273
+ private getSpotMarketAssetAndLiabilityValue(
274
+ marginCategory: MarginCategory,
275
+ marketIndex?: number,
276
+ liquidationBuffer?: BN,
277
+ includeOpenOrders?: boolean,
278
+ strict = false,
279
+ now?: BN
280
+ ): { totalAssetValue: BN; totalLiabilityValue: BN } {
281
+ now = now || new BN(new Date().getTime() / 1000);
282
+ let netQuoteValue = ZERO;
283
+ let totalAssetValue = ZERO;
284
+ let totalLiabilityValue = ZERO;
285
+ for (const spotPosition of this.userAccount!.spotPositions) {
286
+ const countForBase =
287
+ marketIndex === undefined || spotPosition.marketIndex === marketIndex;
288
+
289
+ const countForQuote =
290
+ marketIndex === undefined ||
291
+ marketIndex === QUOTE_SPOT_MARKET_INDEX ||
292
+ (includeOpenOrders && spotPosition.openOrders !== 0);
293
+ if (
294
+ isSpotPositionAvailable(spotPosition) ||
295
+ (!countForBase && !countForQuote)
296
+ ) {
297
+ continue;
298
+ }
299
+
300
+ const spotMarketAccount: SpotMarketAccount =
301
+ this.driftClient.getSpotMarketAccount(spotPosition.marketIndex)!;
302
+
303
+ const oraclePriceData = this.driftClient.getOracleDataForSpotMarket(
304
+ spotPosition.marketIndex
305
+ );
306
+
307
+ let twap5min;
308
+ if (strict) {
309
+ twap5min = calculateLiveOracleTwap(
310
+ spotMarketAccount.historicalOracleData,
311
+ oraclePriceData,
312
+ now,
313
+ FIVE_MINUTE // 5MIN
314
+ );
315
+ }
316
+ const strictOraclePrice = new StrictOraclePrice(
317
+ oraclePriceData.price,
318
+ twap5min
319
+ );
320
+
321
+ if (
322
+ spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX &&
323
+ countForQuote
324
+ ) {
325
+ const tokenAmount = getSignedTokenAmount(
326
+ getTokenAmount(
327
+ spotPosition.scaledBalance,
328
+ spotMarketAccount,
329
+ spotPosition.balanceType
330
+ ),
331
+ spotPosition.balanceType
332
+ );
333
+
334
+ if (isVariant(spotPosition.balanceType, 'borrow')) {
335
+ const weightedTokenValue = this.getSpotLiabilityValue(
336
+ tokenAmount,
337
+ strictOraclePrice,
338
+ spotMarketAccount,
339
+ marginCategory,
340
+ liquidationBuffer
341
+ ).abs();
342
+
343
+ netQuoteValue = netQuoteValue.sub(weightedTokenValue);
344
+ } else {
345
+ const weightedTokenValue = this.getSpotAssetValue(
346
+ tokenAmount,
347
+ strictOraclePrice,
348
+ spotMarketAccount,
349
+ marginCategory
350
+ );
351
+
352
+ netQuoteValue = netQuoteValue.add(weightedTokenValue);
353
+ }
354
+
355
+ continue;
356
+ }
357
+
358
+ if (!includeOpenOrders && countForBase) {
359
+ if (isVariant(spotPosition.balanceType, 'borrow')) {
360
+ const tokenAmount = getSignedTokenAmount(
361
+ getTokenAmount(
362
+ spotPosition.scaledBalance,
363
+ spotMarketAccount,
364
+ spotPosition.balanceType
365
+ ),
366
+ SpotBalanceType.BORROW
367
+ );
368
+ const liabilityValue = this.getSpotLiabilityValue(
369
+ tokenAmount,
370
+ strictOraclePrice,
371
+ spotMarketAccount,
372
+ marginCategory,
373
+ liquidationBuffer
374
+ ).abs();
375
+ totalLiabilityValue = totalLiabilityValue.add(liabilityValue);
376
+
377
+ continue;
378
+ } else {
379
+ const tokenAmount = getTokenAmount(
380
+ spotPosition.scaledBalance,
381
+ spotMarketAccount,
382
+ spotPosition.balanceType
383
+ );
384
+ const assetValue = this.getSpotAssetValue(
385
+ tokenAmount,
386
+ strictOraclePrice,
387
+ spotMarketAccount,
388
+ marginCategory
389
+ );
390
+ totalAssetValue = totalAssetValue.add(assetValue);
391
+
392
+ continue;
393
+ }
394
+ }
395
+
396
+ const {
397
+ tokenAmount: worstCaseTokenAmount,
398
+ ordersValue: worstCaseQuoteTokenAmount,
399
+ } = getWorstCaseTokenAmounts(
400
+ spotPosition,
401
+ spotMarketAccount,
402
+ strictOraclePrice,
403
+ marginCategory,
404
+ this.userAccount!.maxMarginRatio
405
+ );
406
+
407
+ if (worstCaseTokenAmount.gt(ZERO) && countForBase) {
408
+ const baseAssetValue = this.getSpotAssetValue(
409
+ worstCaseTokenAmount,
410
+ strictOraclePrice,
411
+ spotMarketAccount,
412
+ marginCategory
413
+ );
414
+
415
+ totalAssetValue = totalAssetValue.add(baseAssetValue);
416
+ }
417
+
418
+ if (worstCaseTokenAmount.lt(ZERO) && countForBase) {
419
+ const baseLiabilityValue = this.getSpotLiabilityValue(
420
+ worstCaseTokenAmount,
421
+ strictOraclePrice,
422
+ spotMarketAccount,
423
+ marginCategory,
424
+ liquidationBuffer
425
+ ).abs();
426
+
427
+ totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue);
428
+ }
429
+
430
+ if (worstCaseQuoteTokenAmount.gt(ZERO) && countForQuote) {
431
+ netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount);
432
+ }
433
+
434
+ if (worstCaseQuoteTokenAmount.lt(ZERO) && countForQuote) {
435
+ let weight = SPOT_MARKET_WEIGHT_PRECISION;
436
+ if (marginCategory === 'Initial') {
437
+ weight = BN.max(weight, new BN(this.userAccount!.maxMarginRatio));
438
+ }
439
+
440
+ const weightedTokenValue = worstCaseQuoteTokenAmount
441
+ .abs()
442
+ .mul(weight)
443
+ .div(SPOT_MARKET_WEIGHT_PRECISION);
444
+
445
+ netQuoteValue = netQuoteValue.sub(weightedTokenValue);
446
+ }
447
+
448
+ totalLiabilityValue = totalLiabilityValue.add(
449
+ new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
450
+ );
451
+ }
452
+
453
+ if (marketIndex === undefined || marketIndex === QUOTE_SPOT_MARKET_INDEX) {
454
+ if (netQuoteValue.gt(ZERO)) {
455
+ totalAssetValue = totalAssetValue.add(netQuoteValue);
456
+ } else {
457
+ totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs());
458
+ }
459
+ }
460
+
461
+ return { totalAssetValue, totalLiabilityValue };
462
+ }
463
+
464
+ private getSpotLiabilityValue(
465
+ tokenAmount: BN,
466
+ strictOraclePrice: StrictOraclePrice,
467
+ spotMarketAccount: SpotMarketAccount,
468
+ marginCategory?: MarginCategory,
469
+ liquidationBuffer?: BN
470
+ ): BN {
471
+ let liabilityValue = getStrictTokenValue(
472
+ tokenAmount,
473
+ spotMarketAccount.decimals,
474
+ strictOraclePrice
475
+ );
476
+
477
+ if (marginCategory !== undefined) {
478
+ let weight = calculateLiabilityWeight(
479
+ tokenAmount,
480
+ spotMarketAccount,
481
+ marginCategory
482
+ );
483
+
484
+ if (
485
+ marginCategory === 'Initial' &&
486
+ spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX
487
+ ) {
488
+ weight = BN.max(
489
+ weight,
490
+ SPOT_MARKET_WEIGHT_PRECISION.addn(
491
+ this.userAccount!.maxMarginRatio
492
+ )
493
+ );
494
+ }
495
+
496
+ if (liquidationBuffer !== undefined) {
497
+ weight = weight.add(liquidationBuffer);
498
+ }
499
+
500
+ liabilityValue = liabilityValue
501
+ .mul(weight)
502
+ .div(SPOT_MARKET_WEIGHT_PRECISION);
503
+ }
504
+
505
+ return liabilityValue;
506
+ }
507
+
508
+ private getSpotAssetValue(
509
+ tokenAmount: BN,
510
+ strictOraclePrice: StrictOraclePrice,
511
+ spotMarketAccount: SpotMarketAccount,
512
+ marginCategory?: MarginCategory
513
+ ): BN {
514
+ let assetValue = getStrictTokenValue(
515
+ tokenAmount,
516
+ spotMarketAccount.decimals,
517
+ strictOraclePrice
518
+ );
519
+
520
+ if (marginCategory !== undefined) {
521
+ let weight = calculateAssetWeight(
522
+ tokenAmount,
523
+ strictOraclePrice.current,
524
+ spotMarketAccount,
525
+ marginCategory
526
+ );
527
+
528
+ if (
529
+ marginCategory === 'Initial' &&
530
+ spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX
531
+ ) {
532
+ const userCustomAssetWeight = BN.max(
533
+ ZERO,
534
+ SPOT_MARKET_WEIGHT_PRECISION.subn(
535
+ this.userAccount!.maxMarginRatio
536
+ )
537
+ );
538
+ weight = BN.min(weight, userCustomAssetWeight);
539
+ }
540
+
541
+ assetValue = assetValue.mul(weight).div(SPOT_MARKET_WEIGHT_PRECISION);
542
+ }
543
+
544
+ return assetValue;
545
+ }
546
+
547
+ private getUnrealizedPNL(
548
+ withFunding: boolean,
549
+ marketIndex?: number,
550
+ withWeightMarginCategory?: MarginCategory,
551
+ strict = false
552
+ ): BN {
553
+ return this.getActivePerpPositions()
554
+ .filter((pos) =>
555
+ marketIndex !== undefined ? pos.marketIndex === marketIndex : true
556
+ )
557
+ .reduce((unrealizedPnl, perpPosition) => {
558
+ const market = this.driftClient.getPerpMarketAccount(
559
+ perpPosition.marketIndex
560
+ )!;
561
+ const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(
562
+ market.marketIndex
563
+ );
564
+
565
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
566
+ market.quoteSpotMarketIndex
567
+ )!;
568
+ const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(
569
+ market.quoteSpotMarketIndex
570
+ );
571
+
572
+ if (perpPosition.lpShares.gt(ZERO)) {
573
+ perpPosition = this.getPerpPositionWithLPSettle(
574
+ perpPosition.marketIndex,
575
+ undefined,
576
+ !!withWeightMarginCategory
577
+ )[0];
578
+ }
579
+
580
+ let positionUnrealizedPnl = calculatePositionPNL(
581
+ market,
582
+ perpPosition,
583
+ withFunding,
584
+ oraclePriceData
585
+ );
586
+
587
+ let quotePrice;
588
+ if (strict && positionUnrealizedPnl.gt(ZERO)) {
589
+ quotePrice = BN.min(
590
+ quoteOraclePriceData.price,
591
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
592
+ );
593
+ } else if (strict && positionUnrealizedPnl.lt(ZERO)) {
594
+ quotePrice = BN.max(
595
+ quoteOraclePriceData.price,
596
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
597
+ );
598
+ } else {
599
+ quotePrice = quoteOraclePriceData.price;
600
+ }
601
+
602
+ positionUnrealizedPnl = positionUnrealizedPnl
603
+ .mul(quotePrice)
604
+ .div(PRICE_PRECISION);
605
+
606
+ if (withWeightMarginCategory !== undefined) {
607
+ if (positionUnrealizedPnl.gt(ZERO)) {
608
+ positionUnrealizedPnl = positionUnrealizedPnl
609
+ .mul(
610
+ calculateUnrealizedAssetWeight(
611
+ market,
612
+ quoteSpotMarket,
613
+ positionUnrealizedPnl,
614
+ withWeightMarginCategory,
615
+ oraclePriceData
616
+ )
617
+ )
618
+ .div(new BN(SPOT_MARKET_WEIGHT_PRECISION));
619
+ }
620
+ }
621
+
622
+ return unrealizedPnl.add(positionUnrealizedPnl);
623
+ }, ZERO);
624
+ }
625
+
626
+ private getActivePerpPositions() {
627
+ return this.userAccount!.perpPositions.filter(
628
+ (pos) =>
629
+ !pos.baseAssetAmount.eq(ZERO) ||
630
+ !pos.quoteAssetAmount.eq(ZERO) ||
631
+ !(pos.openOrders == 0) ||
632
+ !pos.lpShares.eq(ZERO)
633
+ );
634
+ }
635
+
636
+ private getPerpPositionWithLPSettle(
637
+ marketIndex: number,
638
+ originalPosition?: PerpPosition,
639
+ burnLpShares = false,
640
+ includeRemainderInBaseAmount = false
641
+ ): [PerpPosition, BN, BN] {
642
+ originalPosition =
643
+ originalPosition ??
644
+ this.getPerpPosition(marketIndex) ??
645
+ this.getEmptyPosition(marketIndex);
646
+
647
+ if (originalPosition.lpShares.eq(ZERO)) {
648
+ return [originalPosition, ZERO, ZERO];
649
+ }
650
+
651
+ const position = this.getClonedPosition(originalPosition);
652
+ const market = this.driftClient.getPerpMarketAccount(position.marketIndex)!;
653
+
654
+ if (market.amm.perLpBase != position.perLpBase) {
655
+ // perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares
656
+ const expoDiff = market.amm.perLpBase - position.perLpBase;
657
+ const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff));
658
+
659
+ if (expoDiff > 0) {
660
+ position.lastBaseAssetAmountPerLp =
661
+ position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar);
662
+ position.lastQuoteAssetAmountPerLp =
663
+ position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar);
664
+ } else {
665
+ position.lastBaseAssetAmountPerLp =
666
+ position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar);
667
+ position.lastQuoteAssetAmountPerLp =
668
+ position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar);
669
+ }
670
+
671
+ position.perLpBase = position.perLpBase + expoDiff;
672
+ }
673
+
674
+ const nShares = position.lpShares;
675
+
676
+ // incorp unsettled funding on pre settled position
677
+ const quoteFundingPnl = calculateUnsettledFundingPnl(market, position);
678
+
679
+ let baseUnit = AMM_RESERVE_PRECISION;
680
+ if (market.amm.perLpBase == position.perLpBase) {
681
+ if (
682
+ position.perLpBase >= 0 &&
683
+ position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber()
684
+ ) {
685
+ const marketPerLpRebase = new BN(10 ** market.amm.perLpBase);
686
+ baseUnit = baseUnit.mul(marketPerLpRebase);
687
+ } else if (
688
+ position.perLpBase < 0 &&
689
+ position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber()
690
+ ) {
691
+ const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase));
692
+ baseUnit = baseUnit.div(marketPerLpRebase);
693
+ } else {
694
+ throw 'cannot calc';
695
+ }
696
+ } else {
697
+ throw 'market.amm.perLpBase != position.perLpBase';
698
+ }
699
+
700
+ const deltaBaa = market.amm.baseAssetAmountPerLp
701
+ .sub(position.lastBaseAssetAmountPerLp)
702
+ .mul(nShares)
703
+ .div(baseUnit);
704
+ const deltaQaa = market.amm.quoteAssetAmountPerLp
705
+ .sub(position.lastQuoteAssetAmountPerLp)
706
+ .mul(nShares)
707
+ .div(baseUnit);
708
+
709
+ function sign(v: BN) {
710
+ return v.isNeg() ? new BN(-1) : new BN(1);
711
+ }
712
+
713
+ function standardize(amount: BN, stepSize: BN) {
714
+ const remainder = amount.abs().mod(stepSize).mul(sign(amount));
715
+ const standardizedAmount = amount.sub(remainder);
716
+ return [standardizedAmount, remainder];
717
+ }
718
+
719
+ const [standardizedBaa, remainderBaa] = standardize(
720
+ deltaBaa,
721
+ market.amm.orderStepSize
722
+ );
723
+
724
+ position.remainderBaseAssetAmount += remainderBaa.toNumber();
725
+
726
+ if (
727
+ Math.abs(position.remainderBaseAssetAmount) >
728
+ market.amm.orderStepSize.toNumber()
729
+ ) {
730
+ const [newStandardizedBaa, newRemainderBaa] = standardize(
731
+ new BN(position.remainderBaseAssetAmount),
732
+ market.amm.orderStepSize
733
+ );
734
+ position.baseAssetAmount =
735
+ position.baseAssetAmount.add(newStandardizedBaa);
736
+ position.remainderBaseAssetAmount = newRemainderBaa.toNumber();
737
+ }
738
+
739
+ let dustBaseAssetValue = ZERO;
740
+ if (burnLpShares && position.remainderBaseAssetAmount != 0) {
741
+ const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(
742
+ position.marketIndex
743
+ );
744
+ dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount))
745
+ .mul(oraclePriceData.price)
746
+ .div(AMM_RESERVE_PRECISION)
747
+ .add(ONE);
748
+ }
749
+
750
+ let updateType;
751
+ if (position.baseAssetAmount.eq(ZERO)) {
752
+ updateType = 'open';
753
+ } else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) {
754
+ updateType = 'increase';
755
+ } else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) {
756
+ updateType = 'reduce';
757
+ } else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) {
758
+ updateType = 'close';
759
+ } else {
760
+ updateType = 'flip';
761
+ }
762
+
763
+ let newQuoteEntry;
764
+ let pnl;
765
+ if (updateType == 'open' || updateType == 'increase') {
766
+ newQuoteEntry = position.quoteEntryAmount.add(deltaQaa);
767
+ pnl = ZERO;
768
+ } else if (updateType == 'reduce' || updateType == 'close') {
769
+ newQuoteEntry = position.quoteEntryAmount.sub(
770
+ position.quoteEntryAmount
771
+ .mul(deltaBaa.abs())
772
+ .div(position.baseAssetAmount.abs())
773
+ );
774
+ pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa);
775
+ } else {
776
+ newQuoteEntry = deltaQaa.sub(
777
+ deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs())
778
+ );
779
+ pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry));
780
+ }
781
+ position.quoteEntryAmount = newQuoteEntry;
782
+ position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa);
783
+ position.quoteAssetAmount = position.quoteAssetAmount
784
+ .add(deltaQaa)
785
+ .add(quoteFundingPnl)
786
+ .sub(dustBaseAssetValue);
787
+ position.quoteBreakEvenAmount = position.quoteBreakEvenAmount
788
+ .add(deltaQaa)
789
+ .add(quoteFundingPnl)
790
+ .sub(dustBaseAssetValue);
791
+
792
+ // update open bids/asks
793
+ const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk(
794
+ market.amm.baseAssetReserve,
795
+ market.amm.minBaseAssetReserve,
796
+ market.amm.maxBaseAssetReserve,
797
+ market.amm.orderStepSize
798
+ );
799
+ const lpOpenBids = marketOpenBids
800
+ .mul(position.lpShares)
801
+ .div(market.amm.sqrtK);
802
+ const lpOpenAsks = marketOpenAsks
803
+ .mul(position.lpShares)
804
+ .div(market.amm.sqrtK);
805
+ position.openBids = lpOpenBids.add(position.openBids);
806
+ position.openAsks = lpOpenAsks.add(position.openAsks);
807
+
808
+ // eliminate counting funding on settled position
809
+ if (position.baseAssetAmount.gt(ZERO)) {
810
+ position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong;
811
+ } else if (position.baseAssetAmount.lt(ZERO)) {
812
+ position.lastCumulativeFundingRate =
813
+ market.amm.cumulativeFundingRateShort;
814
+ } else {
815
+ position.lastCumulativeFundingRate = ZERO;
816
+ }
817
+
818
+ const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount);
819
+
820
+ if (includeRemainderInBaseAmount) {
821
+ position.baseAssetAmount = position.baseAssetAmount.add(
822
+ remainderBeforeRemoval
823
+ );
824
+ position.remainderBaseAssetAmount = 0;
825
+ }
826
+
827
+ return [position, remainderBeforeRemoval, pnl];
828
+ }
829
+
830
+ private getPerpPosition(marketIndex: number): PerpPosition | undefined {
831
+ const activePositions = this.userAccount!.perpPositions.filter(
832
+ (pos) =>
833
+ !pos.baseAssetAmount.eq(ZERO) ||
834
+ !pos.quoteAssetAmount.eq(ZERO) ||
835
+ !(pos.openOrders == 0) ||
836
+ !pos.lpShares.eq(ZERO)
837
+ );
838
+ return activePositions.find(
839
+ (position) => position.marketIndex === marketIndex
840
+ );
841
+ }
842
+
843
+ private getEmptyPosition(marketIndex: number): PerpPosition {
844
+ return {
845
+ baseAssetAmount: ZERO,
846
+ remainderBaseAssetAmount: 0,
847
+ lastCumulativeFundingRate: ZERO,
848
+ marketIndex,
849
+ quoteAssetAmount: ZERO,
850
+ quoteEntryAmount: ZERO,
851
+ quoteBreakEvenAmount: ZERO,
852
+ openOrders: 0,
853
+ openBids: ZERO,
854
+ openAsks: ZERO,
855
+ settledPnl: ZERO,
856
+ lpShares: ZERO,
857
+ lastBaseAssetAmountPerLp: ZERO,
858
+ lastQuoteAssetAmountPerLp: ZERO,
859
+ perLpBase: 0,
860
+ };
861
+ }
862
+
863
+ private getClonedPosition(position: PerpPosition): PerpPosition {
864
+ const clonedPosition = Object.assign({}, position);
865
+ return clonedPosition;
866
+ }
867
+
868
+ public getMaintenanceMarginRequirement(): BN {
869
+ if (!this.isInitialized) throw new Error("Drift user not initialized");
870
+
871
+ // if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer
872
+ let liquidationBuffer: BN | undefined = undefined;
873
+ if (this.isBeingLiquidated()) {
874
+ liquidationBuffer = new BN(
875
+ this.driftClient.getStateAccount().liquidationMarginBufferRatio
876
+ );
877
+ }
878
+
879
+ return this.getMarginRequirement('Maintenance', liquidationBuffer);
880
+ }
881
+
882
+ private getMarginRequirement(
883
+ marginCategory: MarginCategory,
884
+ liquidationBuffer?: BN,
885
+ strict = false,
886
+ includeOpenOrders = true
887
+ ): BN {
888
+ return this.getTotalPerpPositionLiability(
889
+ marginCategory,
890
+ liquidationBuffer,
891
+ includeOpenOrders,
892
+ strict
893
+ ).add(
894
+ this.getSpotMarketLiabilityValue(
895
+ marginCategory,
896
+ undefined,
897
+ liquidationBuffer,
898
+ includeOpenOrders,
899
+ strict
900
+ )
901
+ );
902
+ }
903
+
904
+ private getTotalPerpPositionLiability(
905
+ marginCategory?: MarginCategory,
906
+ liquidationBuffer?: BN,
907
+ includeOpenOrders?: boolean,
908
+ strict = false
909
+ ): BN {
910
+ return this.getActivePerpPositions().reduce(
911
+ (totalPerpValue, perpPosition) => {
912
+ const baseAssetValue = this.calculateWeightedPerpPositionLiability(
913
+ perpPosition,
914
+ marginCategory,
915
+ liquidationBuffer,
916
+ includeOpenOrders,
917
+ strict
918
+ );
919
+ return totalPerpValue.add(baseAssetValue);
920
+ },
921
+ ZERO
922
+ );
923
+ }
924
+
925
+ private calculateWeightedPerpPositionLiability(
926
+ perpPosition: PerpPosition,
927
+ marginCategory?: MarginCategory,
928
+ liquidationBuffer?: BN,
929
+ includeOpenOrders?: boolean,
930
+ strict = false
931
+ ): BN {
932
+ const market = this.driftClient.getPerpMarketAccount(
933
+ perpPosition.marketIndex
934
+ )!;
935
+
936
+ if (perpPosition.lpShares.gt(ZERO)) {
937
+ // is an lp, clone so we dont mutate the position
938
+ perpPosition = this.getPerpPositionWithLPSettle(
939
+ market.marketIndex,
940
+ this.getClonedPosition(perpPosition),
941
+ !!marginCategory
942
+ )[0];
943
+ }
944
+
945
+ let valuationPrice = this.driftClient.getOracleDataForPerpMarket(
946
+ market.marketIndex
947
+ ).price;
948
+
949
+ if (isVariant(market.status, 'settlement')) {
950
+ valuationPrice = market.expiryPrice;
951
+ }
952
+
953
+ let baseAssetAmount: BN;
954
+ let liabilityValue;
955
+ if (includeOpenOrders) {
956
+ const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } =
957
+ calculateWorstCasePerpLiabilityValue(
958
+ perpPosition,
959
+ market,
960
+ valuationPrice
961
+ );
962
+ baseAssetAmount = worstCaseBaseAssetAmount;
963
+ liabilityValue = worstCaseLiabilityValue;
964
+ } else {
965
+ baseAssetAmount = perpPosition.baseAssetAmount;
966
+ liabilityValue = calculatePerpLiabilityValue(
967
+ baseAssetAmount,
968
+ valuationPrice,
969
+ isVariant(market.contractType, 'prediction')
970
+ );
971
+ }
972
+
973
+ if (marginCategory) {
974
+ let marginRatio = new BN(
975
+ calculateMarketMarginRatio(
976
+ market,
977
+ baseAssetAmount.abs(),
978
+ marginCategory,
979
+ this.userAccount!.maxMarginRatio,
980
+ isVariant(this.userAccount!.marginMode, 'highLeverage')
981
+ )
982
+ );
983
+
984
+ if (liquidationBuffer !== undefined) {
985
+ marginRatio = marginRatio.add(liquidationBuffer);
986
+ }
987
+
988
+ if (isVariant(market.status, 'settlement')) {
989
+ marginRatio = ZERO;
990
+ }
991
+
992
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
993
+ market.quoteSpotMarketIndex
994
+ )!;
995
+ const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(
996
+ QUOTE_SPOT_MARKET_INDEX
997
+ );
998
+
999
+ let quotePrice;
1000
+ if (strict) {
1001
+ quotePrice = BN.max(
1002
+ quoteOraclePriceData.price,
1003
+ quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
1004
+ );
1005
+ } else {
1006
+ quotePrice = quoteOraclePriceData.price;
1007
+ }
1008
+
1009
+ liabilityValue = liabilityValue
1010
+ .mul(quotePrice)
1011
+ .div(PRICE_PRECISION)
1012
+ .mul(marginRatio)
1013
+ .div(MARGIN_PRECISION);
1014
+
1015
+ if (includeOpenOrders) {
1016
+ liabilityValue = liabilityValue.add(
1017
+ new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
1018
+ );
1019
+
1020
+ if (perpPosition.lpShares.gt(ZERO)) {
1021
+ liabilityValue = liabilityValue.add(
1022
+ BN.max(
1023
+ QUOTE_PRECISION,
1024
+ valuationPrice
1025
+ .mul(market.amm.orderStepSize)
1026
+ .mul(QUOTE_PRECISION)
1027
+ .div(AMM_RESERVE_PRECISION)
1028
+ .div(PRICE_PRECISION)
1029
+ )
1030
+ );
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ return liabilityValue;
1036
+ }
1037
+
1038
+ private getSpotMarketLiabilityValue(
1039
+ marginCategory: MarginCategory,
1040
+ marketIndex?: number,
1041
+ liquidationBuffer?: BN,
1042
+ includeOpenOrders?: boolean,
1043
+ strict = false,
1044
+ now?: BN
1045
+ ): BN {
1046
+ const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(
1047
+ marginCategory,
1048
+ marketIndex,
1049
+ liquidationBuffer,
1050
+ includeOpenOrders,
1051
+ strict,
1052
+ now
1053
+ );
1054
+ return totalLiabilityValue;
1055
+ }
1056
+ }