@quartz-labs/sdk 0.0.2 → 0.0.4

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