@quartz-labs/sdk 0.0.1 → 0.0.2

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,567 @@
1
+ import { AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_EXP, BN, calculateAssetWeight, calculateLiabilityWeight, calculateLiveOracleTwap, calculateMarketMarginRatio, calculateMarketOpenBidAsk, calculatePerpLiabilityValue, calculatePositionPNL, calculateUnrealizedAssetWeight, calculateUnsettledFundingPnl, calculateWithdrawLimit, calculateWorstCasePerpLiabilityValue, fetchUserAccountsUsingKeys, FIVE_MINUTE, getSignedTokenAmount, getStrictTokenValue, getTokenAmount, getWorstCaseTokenAmounts, isSpotPositionAvailable, isVariant, MARGIN_PRECISION, ONE, OPEN_ORDER_MARGIN_REQUIREMENT, PRICE_PRECISION, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, SPOT_MARKET_WEIGHT_PRECISION, SpotBalanceType, StrictOraclePrice, UserStatus, ZERO, TEN, divCeil } from "@drift-labs/sdk";
2
+ import { getDriftUser } from "../helpers.js";
3
+ export class DriftUser {
4
+ constructor(authority, connection, driftClient, userAccount) {
5
+ this.isInitialized = false;
6
+ this.authority = authority;
7
+ this.connection = connection;
8
+ this.driftClient = driftClient;
9
+ if (userAccount) {
10
+ this.userAccount = userAccount;
11
+ this.isInitialized = true;
12
+ }
13
+ }
14
+ async initialize() {
15
+ if (this.isInitialized)
16
+ return;
17
+ const [userAccount] = await fetchUserAccountsUsingKeys(this.connection, this.driftClient.program, [getDriftUser(this.authority)]);
18
+ if (!userAccount)
19
+ throw new Error("Drift user not found");
20
+ this.userAccount = userAccount;
21
+ this.isInitialized = true;
22
+ }
23
+ getHealth() {
24
+ if (!this.isInitialized)
25
+ throw new Error("DriftUser not initialized");
26
+ if (this.isBeingLiquidated())
27
+ return 0;
28
+ const totalCollateral = this.getTotalCollateral('Maintenance');
29
+ const maintenanceMarginReq = this.getMaintenanceMarginRequirement();
30
+ if (maintenanceMarginReq.eq(ZERO) && totalCollateral.gte(ZERO)) {
31
+ return 100;
32
+ }
33
+ if (totalCollateral.lte(ZERO)) {
34
+ return 0;
35
+ }
36
+ return Math.round(Math.min(100, Math.max(0, (1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) * 100)));
37
+ }
38
+ getTokenAmount(marketIndex) {
39
+ if (!this.isInitialized)
40
+ throw new Error("DriftUser not initialized");
41
+ const spotPosition = this.userAccount.spotPositions.find((position) => position.marketIndex === marketIndex);
42
+ if (spotPosition === undefined) {
43
+ return ZERO;
44
+ }
45
+ const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
46
+ return getSignedTokenAmount(getTokenAmount(spotPosition.scaledBalance, spotMarket, spotPosition.balanceType), spotPosition.balanceType);
47
+ }
48
+ getWithdrawalLimit(marketIndex, reduceOnly) {
49
+ const nowTs = new BN(Math.floor(Date.now() / 1000));
50
+ const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
51
+ // eslint-disable-next-line prefer-const
52
+ let { borrowLimit, withdrawLimit } = calculateWithdrawLimit(spotMarket, nowTs);
53
+ const freeCollateral = this.getFreeCollateral();
54
+ const initialMarginRequirement = this.getMarginRequirement('Initial', undefined, false);
55
+ const oracleData = this.driftClient.getOracleDataForSpotMarket(marketIndex);
56
+ const precisionIncrease = TEN.pow(new BN(spotMarket.decimals - 6));
57
+ const { canBypass, depositAmount: userDepositAmount } = this.canBypassWithdrawLimits(marketIndex);
58
+ if (canBypass) {
59
+ withdrawLimit = BN.max(withdrawLimit, userDepositAmount);
60
+ }
61
+ const assetWeight = calculateAssetWeight(userDepositAmount, oracleData.price, spotMarket, 'Initial');
62
+ let amountWithdrawable;
63
+ if (assetWeight.eq(ZERO)) {
64
+ amountWithdrawable = userDepositAmount;
65
+ }
66
+ else if (initialMarginRequirement.eq(ZERO)) {
67
+ amountWithdrawable = userDepositAmount;
68
+ }
69
+ else {
70
+ amountWithdrawable = divCeil(divCeil(freeCollateral.mul(MARGIN_PRECISION), assetWeight).mul(PRICE_PRECISION), oracleData.price).mul(precisionIncrease);
71
+ }
72
+ const maxWithdrawValue = BN.min(BN.min(amountWithdrawable, userDepositAmount), withdrawLimit.abs());
73
+ if (reduceOnly) {
74
+ return BN.max(maxWithdrawValue, ZERO);
75
+ }
76
+ else {
77
+ const weightedAssetValue = this.getSpotMarketAssetValue('Initial', marketIndex, false);
78
+ const freeCollatAfterWithdraw = userDepositAmount.gt(ZERO)
79
+ ? freeCollateral.sub(weightedAssetValue)
80
+ : freeCollateral;
81
+ const maxLiabilityAllowed = freeCollatAfterWithdraw
82
+ .mul(MARGIN_PRECISION)
83
+ .div(new BN(spotMarket.initialLiabilityWeight))
84
+ .mul(PRICE_PRECISION)
85
+ .div(oracleData.price)
86
+ .mul(precisionIncrease);
87
+ const maxBorrowValue = BN.min(maxWithdrawValue.add(maxLiabilityAllowed), borrowLimit.abs());
88
+ return BN.max(maxBorrowValue, ZERO);
89
+ }
90
+ }
91
+ getFreeCollateral(marginCategory = 'Initial') {
92
+ const totalCollateral = this.getTotalCollateral(marginCategory, true);
93
+ const marginRequirement = marginCategory === 'Initial'
94
+ ? this.getMarginRequirement('Initial', undefined, false)
95
+ : this.getMaintenanceMarginRequirement();
96
+ const freeCollateral = totalCollateral.sub(marginRequirement);
97
+ return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
98
+ }
99
+ canBypassWithdrawLimits(marketIndex) {
100
+ const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
101
+ const maxDepositAmount = spotMarket.withdrawGuardThreshold.div(new BN(10));
102
+ const position = this.userAccount.spotPositions.find((position) => position.marketIndex === marketIndex);
103
+ const netDeposits = this.userAccount.totalDeposits.sub(this.userAccount.totalWithdraws);
104
+ if (!position) {
105
+ return {
106
+ canBypass: false,
107
+ maxDepositAmount,
108
+ depositAmount: ZERO,
109
+ netDeposits,
110
+ };
111
+ }
112
+ if (isVariant(position.balanceType, 'borrow')) {
113
+ return {
114
+ canBypass: false,
115
+ maxDepositAmount,
116
+ netDeposits,
117
+ depositAmount: ZERO,
118
+ };
119
+ }
120
+ const depositAmount = getTokenAmount(position.scaledBalance, spotMarket, SpotBalanceType.DEPOSIT);
121
+ if (netDeposits.lt(ZERO)) {
122
+ return {
123
+ canBypass: false,
124
+ maxDepositAmount,
125
+ depositAmount,
126
+ netDeposits,
127
+ };
128
+ }
129
+ return {
130
+ canBypass: depositAmount.lt(maxDepositAmount),
131
+ maxDepositAmount,
132
+ netDeposits,
133
+ depositAmount,
134
+ };
135
+ }
136
+ isBeingLiquidated() {
137
+ return ((this.userAccount.status &
138
+ (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
139
+ 0);
140
+ }
141
+ getTotalCollateral(marginCategory = 'Initial', strict = false, includeOpenOrders = true) {
142
+ if (!this.isInitialized)
143
+ throw new Error("DriftUser not initialized");
144
+ return this.getSpotMarketAssetValue(marginCategory, undefined, includeOpenOrders, strict).add(this.getUnrealizedPNL(true, undefined, marginCategory, strict));
145
+ }
146
+ getSpotMarketAssetValue(marginCategory, marketIndex, includeOpenOrders, strict = false, now) {
147
+ const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(marginCategory, marketIndex, undefined, includeOpenOrders, strict, now);
148
+ return totalAssetValue;
149
+ }
150
+ getSpotMarketAssetAndLiabilityValue(marginCategory, marketIndex, liquidationBuffer, includeOpenOrders, strict = false, now) {
151
+ now = now || new BN(new Date().getTime() / 1000);
152
+ let netQuoteValue = ZERO;
153
+ let totalAssetValue = ZERO;
154
+ let totalLiabilityValue = ZERO;
155
+ for (const spotPosition of this.userAccount.spotPositions) {
156
+ const countForBase = marketIndex === undefined || spotPosition.marketIndex === marketIndex;
157
+ const countForQuote = marketIndex === undefined ||
158
+ marketIndex === QUOTE_SPOT_MARKET_INDEX ||
159
+ (includeOpenOrders && spotPosition.openOrders !== 0);
160
+ if (isSpotPositionAvailable(spotPosition) ||
161
+ (!countForBase && !countForQuote)) {
162
+ continue;
163
+ }
164
+ const spotMarketAccount = this.driftClient.getSpotMarketAccount(spotPosition.marketIndex);
165
+ const oraclePriceData = this.driftClient.getOracleDataForSpotMarket(spotPosition.marketIndex);
166
+ let twap5min;
167
+ if (strict) {
168
+ twap5min = calculateLiveOracleTwap(spotMarketAccount.historicalOracleData, oraclePriceData, now, FIVE_MINUTE // 5MIN
169
+ );
170
+ }
171
+ const strictOraclePrice = new StrictOraclePrice(oraclePriceData.price, twap5min);
172
+ if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX &&
173
+ countForQuote) {
174
+ const tokenAmount = getSignedTokenAmount(getTokenAmount(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), spotPosition.balanceType);
175
+ if (isVariant(spotPosition.balanceType, 'borrow')) {
176
+ const weightedTokenValue = this.getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs();
177
+ netQuoteValue = netQuoteValue.sub(weightedTokenValue);
178
+ }
179
+ else {
180
+ const weightedTokenValue = this.getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory);
181
+ netQuoteValue = netQuoteValue.add(weightedTokenValue);
182
+ }
183
+ continue;
184
+ }
185
+ if (!includeOpenOrders && countForBase) {
186
+ if (isVariant(spotPosition.balanceType, 'borrow')) {
187
+ const tokenAmount = getSignedTokenAmount(getTokenAmount(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), SpotBalanceType.BORROW);
188
+ const liabilityValue = this.getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs();
189
+ totalLiabilityValue = totalLiabilityValue.add(liabilityValue);
190
+ continue;
191
+ }
192
+ else {
193
+ const tokenAmount = getTokenAmount(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType);
194
+ const assetValue = this.getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory);
195
+ totalAssetValue = totalAssetValue.add(assetValue);
196
+ continue;
197
+ }
198
+ }
199
+ const { tokenAmount: worstCaseTokenAmount, ordersValue: worstCaseQuoteTokenAmount, } = getWorstCaseTokenAmounts(spotPosition, spotMarketAccount, strictOraclePrice, marginCategory, this.userAccount.maxMarginRatio);
200
+ if (worstCaseTokenAmount.gt(ZERO) && countForBase) {
201
+ const baseAssetValue = this.getSpotAssetValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory);
202
+ totalAssetValue = totalAssetValue.add(baseAssetValue);
203
+ }
204
+ if (worstCaseTokenAmount.lt(ZERO) && countForBase) {
205
+ const baseLiabilityValue = this.getSpotLiabilityValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs();
206
+ totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue);
207
+ }
208
+ if (worstCaseQuoteTokenAmount.gt(ZERO) && countForQuote) {
209
+ netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount);
210
+ }
211
+ if (worstCaseQuoteTokenAmount.lt(ZERO) && countForQuote) {
212
+ let weight = SPOT_MARKET_WEIGHT_PRECISION;
213
+ if (marginCategory === 'Initial') {
214
+ weight = BN.max(weight, new BN(this.userAccount.maxMarginRatio));
215
+ }
216
+ const weightedTokenValue = worstCaseQuoteTokenAmount
217
+ .abs()
218
+ .mul(weight)
219
+ .div(SPOT_MARKET_WEIGHT_PRECISION);
220
+ netQuoteValue = netQuoteValue.sub(weightedTokenValue);
221
+ }
222
+ totalLiabilityValue = totalLiabilityValue.add(new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT));
223
+ }
224
+ if (marketIndex === undefined || marketIndex === QUOTE_SPOT_MARKET_INDEX) {
225
+ if (netQuoteValue.gt(ZERO)) {
226
+ totalAssetValue = totalAssetValue.add(netQuoteValue);
227
+ }
228
+ else {
229
+ totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs());
230
+ }
231
+ }
232
+ return { totalAssetValue, totalLiabilityValue };
233
+ }
234
+ getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer) {
235
+ let liabilityValue = getStrictTokenValue(tokenAmount, spotMarketAccount.decimals, strictOraclePrice);
236
+ if (marginCategory !== undefined) {
237
+ let weight = calculateLiabilityWeight(tokenAmount, spotMarketAccount, marginCategory);
238
+ if (marginCategory === 'Initial' &&
239
+ spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX) {
240
+ weight = BN.max(weight, SPOT_MARKET_WEIGHT_PRECISION.addn(this.userAccount.maxMarginRatio));
241
+ }
242
+ if (liquidationBuffer !== undefined) {
243
+ weight = weight.add(liquidationBuffer);
244
+ }
245
+ liabilityValue = liabilityValue
246
+ .mul(weight)
247
+ .div(SPOT_MARKET_WEIGHT_PRECISION);
248
+ }
249
+ return liabilityValue;
250
+ }
251
+ getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory) {
252
+ let assetValue = getStrictTokenValue(tokenAmount, spotMarketAccount.decimals, strictOraclePrice);
253
+ if (marginCategory !== undefined) {
254
+ let weight = calculateAssetWeight(tokenAmount, strictOraclePrice.current, spotMarketAccount, marginCategory);
255
+ if (marginCategory === 'Initial' &&
256
+ spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX) {
257
+ const userCustomAssetWeight = BN.max(ZERO, SPOT_MARKET_WEIGHT_PRECISION.subn(this.userAccount.maxMarginRatio));
258
+ weight = BN.min(weight, userCustomAssetWeight);
259
+ }
260
+ assetValue = assetValue.mul(weight).div(SPOT_MARKET_WEIGHT_PRECISION);
261
+ }
262
+ return assetValue;
263
+ }
264
+ getUnrealizedPNL(withFunding, marketIndex, withWeightMarginCategory, strict = false) {
265
+ return this.getActivePerpPositions()
266
+ .filter((pos) => marketIndex !== undefined ? pos.marketIndex === marketIndex : true)
267
+ .reduce((unrealizedPnl, perpPosition) => {
268
+ const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
269
+ const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(market.marketIndex);
270
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex);
271
+ const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(market.quoteSpotMarketIndex);
272
+ if (perpPosition.lpShares.gt(ZERO)) {
273
+ perpPosition = this.getPerpPositionWithLPSettle(perpPosition.marketIndex, undefined, !!withWeightMarginCategory)[0];
274
+ }
275
+ let positionUnrealizedPnl = calculatePositionPNL(market, perpPosition, withFunding, oraclePriceData);
276
+ let quotePrice;
277
+ if (strict && positionUnrealizedPnl.gt(ZERO)) {
278
+ quotePrice = BN.min(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min);
279
+ }
280
+ else if (strict && positionUnrealizedPnl.lt(ZERO)) {
281
+ quotePrice = BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min);
282
+ }
283
+ else {
284
+ quotePrice = quoteOraclePriceData.price;
285
+ }
286
+ positionUnrealizedPnl = positionUnrealizedPnl
287
+ .mul(quotePrice)
288
+ .div(PRICE_PRECISION);
289
+ if (withWeightMarginCategory !== undefined) {
290
+ if (positionUnrealizedPnl.gt(ZERO)) {
291
+ positionUnrealizedPnl = positionUnrealizedPnl
292
+ .mul(calculateUnrealizedAssetWeight(market, quoteSpotMarket, positionUnrealizedPnl, withWeightMarginCategory, oraclePriceData))
293
+ .div(new BN(SPOT_MARKET_WEIGHT_PRECISION));
294
+ }
295
+ }
296
+ return unrealizedPnl.add(positionUnrealizedPnl);
297
+ }, ZERO);
298
+ }
299
+ getActivePerpPositions() {
300
+ return this.userAccount.perpPositions.filter((pos) => !pos.baseAssetAmount.eq(ZERO) ||
301
+ !pos.quoteAssetAmount.eq(ZERO) ||
302
+ !(pos.openOrders == 0) ||
303
+ !pos.lpShares.eq(ZERO));
304
+ }
305
+ getPerpPositionWithLPSettle(marketIndex, originalPosition, burnLpShares = false, includeRemainderInBaseAmount = false) {
306
+ originalPosition =
307
+ originalPosition ??
308
+ this.getPerpPosition(marketIndex) ??
309
+ this.getEmptyPosition(marketIndex);
310
+ if (originalPosition.lpShares.eq(ZERO)) {
311
+ return [originalPosition, ZERO, ZERO];
312
+ }
313
+ const position = this.getClonedPosition(originalPosition);
314
+ const market = this.driftClient.getPerpMarketAccount(position.marketIndex);
315
+ if (market.amm.perLpBase != position.perLpBase) {
316
+ // perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares
317
+ const expoDiff = market.amm.perLpBase - position.perLpBase;
318
+ const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff));
319
+ if (expoDiff > 0) {
320
+ position.lastBaseAssetAmountPerLp =
321
+ position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar);
322
+ position.lastQuoteAssetAmountPerLp =
323
+ position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar);
324
+ }
325
+ else {
326
+ position.lastBaseAssetAmountPerLp =
327
+ position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar);
328
+ position.lastQuoteAssetAmountPerLp =
329
+ position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar);
330
+ }
331
+ position.perLpBase = position.perLpBase + expoDiff;
332
+ }
333
+ const nShares = position.lpShares;
334
+ // incorp unsettled funding on pre settled position
335
+ const quoteFundingPnl = calculateUnsettledFundingPnl(market, position);
336
+ let baseUnit = AMM_RESERVE_PRECISION;
337
+ if (market.amm.perLpBase == position.perLpBase) {
338
+ if (position.perLpBase >= 0 &&
339
+ position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber()) {
340
+ const marketPerLpRebase = new BN(10 ** market.amm.perLpBase);
341
+ baseUnit = baseUnit.mul(marketPerLpRebase);
342
+ }
343
+ else if (position.perLpBase < 0 &&
344
+ position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber()) {
345
+ const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase));
346
+ baseUnit = baseUnit.div(marketPerLpRebase);
347
+ }
348
+ else {
349
+ throw 'cannot calc';
350
+ }
351
+ }
352
+ else {
353
+ throw 'market.amm.perLpBase != position.perLpBase';
354
+ }
355
+ const deltaBaa = market.amm.baseAssetAmountPerLp
356
+ .sub(position.lastBaseAssetAmountPerLp)
357
+ .mul(nShares)
358
+ .div(baseUnit);
359
+ const deltaQaa = market.amm.quoteAssetAmountPerLp
360
+ .sub(position.lastQuoteAssetAmountPerLp)
361
+ .mul(nShares)
362
+ .div(baseUnit);
363
+ function sign(v) {
364
+ return v.isNeg() ? new BN(-1) : new BN(1);
365
+ }
366
+ function standardize(amount, stepSize) {
367
+ const remainder = amount.abs().mod(stepSize).mul(sign(amount));
368
+ const standardizedAmount = amount.sub(remainder);
369
+ return [standardizedAmount, remainder];
370
+ }
371
+ const [standardizedBaa, remainderBaa] = standardize(deltaBaa, market.amm.orderStepSize);
372
+ position.remainderBaseAssetAmount += remainderBaa.toNumber();
373
+ if (Math.abs(position.remainderBaseAssetAmount) >
374
+ market.amm.orderStepSize.toNumber()) {
375
+ const [newStandardizedBaa, newRemainderBaa] = standardize(new BN(position.remainderBaseAssetAmount), market.amm.orderStepSize);
376
+ position.baseAssetAmount =
377
+ position.baseAssetAmount.add(newStandardizedBaa);
378
+ position.remainderBaseAssetAmount = newRemainderBaa.toNumber();
379
+ }
380
+ let dustBaseAssetValue = ZERO;
381
+ if (burnLpShares && position.remainderBaseAssetAmount != 0) {
382
+ const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(position.marketIndex);
383
+ dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount))
384
+ .mul(oraclePriceData.price)
385
+ .div(AMM_RESERVE_PRECISION)
386
+ .add(ONE);
387
+ }
388
+ let updateType;
389
+ if (position.baseAssetAmount.eq(ZERO)) {
390
+ updateType = 'open';
391
+ }
392
+ else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) {
393
+ updateType = 'increase';
394
+ }
395
+ else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) {
396
+ updateType = 'reduce';
397
+ }
398
+ else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) {
399
+ updateType = 'close';
400
+ }
401
+ else {
402
+ updateType = 'flip';
403
+ }
404
+ let newQuoteEntry;
405
+ let pnl;
406
+ if (updateType == 'open' || updateType == 'increase') {
407
+ newQuoteEntry = position.quoteEntryAmount.add(deltaQaa);
408
+ pnl = ZERO;
409
+ }
410
+ else if (updateType == 'reduce' || updateType == 'close') {
411
+ newQuoteEntry = position.quoteEntryAmount.sub(position.quoteEntryAmount
412
+ .mul(deltaBaa.abs())
413
+ .div(position.baseAssetAmount.abs()));
414
+ pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa);
415
+ }
416
+ else {
417
+ newQuoteEntry = deltaQaa.sub(deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs()));
418
+ pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry));
419
+ }
420
+ position.quoteEntryAmount = newQuoteEntry;
421
+ position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa);
422
+ position.quoteAssetAmount = position.quoteAssetAmount
423
+ .add(deltaQaa)
424
+ .add(quoteFundingPnl)
425
+ .sub(dustBaseAssetValue);
426
+ position.quoteBreakEvenAmount = position.quoteBreakEvenAmount
427
+ .add(deltaQaa)
428
+ .add(quoteFundingPnl)
429
+ .sub(dustBaseAssetValue);
430
+ // update open bids/asks
431
+ const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk(market.amm.baseAssetReserve, market.amm.minBaseAssetReserve, market.amm.maxBaseAssetReserve, market.amm.orderStepSize);
432
+ const lpOpenBids = marketOpenBids
433
+ .mul(position.lpShares)
434
+ .div(market.amm.sqrtK);
435
+ const lpOpenAsks = marketOpenAsks
436
+ .mul(position.lpShares)
437
+ .div(market.amm.sqrtK);
438
+ position.openBids = lpOpenBids.add(position.openBids);
439
+ position.openAsks = lpOpenAsks.add(position.openAsks);
440
+ // eliminate counting funding on settled position
441
+ if (position.baseAssetAmount.gt(ZERO)) {
442
+ position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong;
443
+ }
444
+ else if (position.baseAssetAmount.lt(ZERO)) {
445
+ position.lastCumulativeFundingRate =
446
+ market.amm.cumulativeFundingRateShort;
447
+ }
448
+ else {
449
+ position.lastCumulativeFundingRate = ZERO;
450
+ }
451
+ const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount);
452
+ if (includeRemainderInBaseAmount) {
453
+ position.baseAssetAmount = position.baseAssetAmount.add(remainderBeforeRemoval);
454
+ position.remainderBaseAssetAmount = 0;
455
+ }
456
+ return [position, remainderBeforeRemoval, pnl];
457
+ }
458
+ getPerpPosition(marketIndex) {
459
+ const activePositions = this.userAccount.perpPositions.filter((pos) => !pos.baseAssetAmount.eq(ZERO) ||
460
+ !pos.quoteAssetAmount.eq(ZERO) ||
461
+ !(pos.openOrders == 0) ||
462
+ !pos.lpShares.eq(ZERO));
463
+ return activePositions.find((position) => position.marketIndex === marketIndex);
464
+ }
465
+ getEmptyPosition(marketIndex) {
466
+ return {
467
+ baseAssetAmount: ZERO,
468
+ remainderBaseAssetAmount: 0,
469
+ lastCumulativeFundingRate: ZERO,
470
+ marketIndex,
471
+ quoteAssetAmount: ZERO,
472
+ quoteEntryAmount: ZERO,
473
+ quoteBreakEvenAmount: ZERO,
474
+ openOrders: 0,
475
+ openBids: ZERO,
476
+ openAsks: ZERO,
477
+ settledPnl: ZERO,
478
+ lpShares: ZERO,
479
+ lastBaseAssetAmountPerLp: ZERO,
480
+ lastQuoteAssetAmountPerLp: ZERO,
481
+ perLpBase: 0,
482
+ };
483
+ }
484
+ getClonedPosition(position) {
485
+ const clonedPosition = Object.assign({}, position);
486
+ return clonedPosition;
487
+ }
488
+ getMaintenanceMarginRequirement() {
489
+ if (!this.isInitialized)
490
+ throw new Error("Drift user not initialized");
491
+ // if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer
492
+ let liquidationBuffer = undefined;
493
+ if (this.isBeingLiquidated()) {
494
+ liquidationBuffer = new BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio);
495
+ }
496
+ return this.getMarginRequirement('Maintenance', liquidationBuffer);
497
+ }
498
+ getMarginRequirement(marginCategory, liquidationBuffer, strict = false, includeOpenOrders = true) {
499
+ return this.getTotalPerpPositionLiability(marginCategory, liquidationBuffer, includeOpenOrders, strict).add(this.getSpotMarketLiabilityValue(marginCategory, undefined, liquidationBuffer, includeOpenOrders, strict));
500
+ }
501
+ getTotalPerpPositionLiability(marginCategory, liquidationBuffer, includeOpenOrders, strict = false) {
502
+ return this.getActivePerpPositions().reduce((totalPerpValue, perpPosition) => {
503
+ const baseAssetValue = this.calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict);
504
+ return totalPerpValue.add(baseAssetValue);
505
+ }, ZERO);
506
+ }
507
+ calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict = false) {
508
+ const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
509
+ if (perpPosition.lpShares.gt(ZERO)) {
510
+ // is an lp, clone so we dont mutate the position
511
+ perpPosition = this.getPerpPositionWithLPSettle(market.marketIndex, this.getClonedPosition(perpPosition), !!marginCategory)[0];
512
+ }
513
+ let valuationPrice = this.driftClient.getOracleDataForPerpMarket(market.marketIndex).price;
514
+ if (isVariant(market.status, 'settlement')) {
515
+ valuationPrice = market.expiryPrice;
516
+ }
517
+ let baseAssetAmount;
518
+ let liabilityValue;
519
+ if (includeOpenOrders) {
520
+ const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = calculateWorstCasePerpLiabilityValue(perpPosition, market, valuationPrice);
521
+ baseAssetAmount = worstCaseBaseAssetAmount;
522
+ liabilityValue = worstCaseLiabilityValue;
523
+ }
524
+ else {
525
+ baseAssetAmount = perpPosition.baseAssetAmount;
526
+ liabilityValue = calculatePerpLiabilityValue(baseAssetAmount, valuationPrice, isVariant(market.contractType, 'prediction'));
527
+ }
528
+ if (marginCategory) {
529
+ let marginRatio = new BN(calculateMarketMarginRatio(market, baseAssetAmount.abs(), marginCategory, this.userAccount.maxMarginRatio, isVariant(this.userAccount.marginMode, 'highLeverage')));
530
+ if (liquidationBuffer !== undefined) {
531
+ marginRatio = marginRatio.add(liquidationBuffer);
532
+ }
533
+ if (isVariant(market.status, 'settlement')) {
534
+ marginRatio = ZERO;
535
+ }
536
+ const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex);
537
+ const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(QUOTE_SPOT_MARKET_INDEX);
538
+ let quotePrice;
539
+ if (strict) {
540
+ quotePrice = BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min);
541
+ }
542
+ else {
543
+ quotePrice = quoteOraclePriceData.price;
544
+ }
545
+ liabilityValue = liabilityValue
546
+ .mul(quotePrice)
547
+ .div(PRICE_PRECISION)
548
+ .mul(marginRatio)
549
+ .div(MARGIN_PRECISION);
550
+ if (includeOpenOrders) {
551
+ liabilityValue = liabilityValue.add(new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT));
552
+ if (perpPosition.lpShares.gt(ZERO)) {
553
+ liabilityValue = liabilityValue.add(BN.max(QUOTE_PRECISION, valuationPrice
554
+ .mul(market.amm.orderStepSize)
555
+ .mul(QUOTE_PRECISION)
556
+ .div(AMM_RESERVE_PRECISION)
557
+ .div(PRICE_PRECISION)));
558
+ }
559
+ }
560
+ }
561
+ return liabilityValue;
562
+ }
563
+ getSpotMarketLiabilityValue(marginCategory, marketIndex, liquidationBuffer, includeOpenOrders, strict = false, now) {
564
+ const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marginCategory, marketIndex, liquidationBuffer, includeOpenOrders, strict, now);
565
+ return totalLiabilityValue;
566
+ }
567
+ }
@@ -0,0 +1,9 @@
1
+ import { DriftClient } from "@drift-labs/sdk";
2
+ import { Connection } from "@solana/web3.js";
3
+ export declare class DriftClientService {
4
+ private static instance;
5
+ private driftClient;
6
+ private driftClientInitPromise;
7
+ constructor(connection: Connection);
8
+ static getDriftClient(connection: Connection): Promise<DriftClient>;
9
+ }
@@ -0,0 +1,28 @@
1
+ import { DriftClient, Wallet } from "@drift-labs/sdk";
2
+ import { Keypair } from "@solana/web3.js";
3
+ import { SUPPORTED_DRIFT_MARKETS } from "../config/constants.js";
4
+ export class DriftClientService {
5
+ constructor(connection) {
6
+ const wallet = new Wallet(Keypair.generate());
7
+ this.driftClient = new DriftClient({
8
+ connection: connection,
9
+ wallet: wallet,
10
+ env: 'mainnet-beta',
11
+ userStats: false,
12
+ perpMarketIndexes: [],
13
+ spotMarketIndexes: SUPPORTED_DRIFT_MARKETS,
14
+ accountSubscription: {
15
+ type: 'websocket',
16
+ commitment: "confirmed"
17
+ }
18
+ });
19
+ this.driftClientInitPromise = this.driftClient.subscribe();
20
+ }
21
+ static async getDriftClient(connection) {
22
+ if (!DriftClientService.instance) {
23
+ DriftClientService.instance = new DriftClientService(connection);
24
+ }
25
+ await DriftClientService.instance.driftClientInitPromise;
26
+ return DriftClientService.instance.driftClient;
27
+ }
28
+ }