@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.
- package/README.md +1 -1
- package/build/client.d.ts +19 -0
- package/build/client.js +60 -0
- package/build/config/constants.d.ts +7 -0
- package/build/config/constants.js +7 -0
- package/build/helpers.d.ts +12 -0
- package/build/helpers.js +39 -0
- package/build/idl/quartz.json +646 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +4 -0
- package/build/model/driftUser.d.ts +33 -0
- package/build/model/driftUser.js +567 -0
- package/build/services/driftClientService.d.ts +9 -0
- package/build/services/driftClientService.js +28 -0
- package/build/types/quartz.d.ts +647 -0
- package/build/types/quartz.js +646 -0
- package/build/user.d.ts +16 -0
- package/build/user.js +47 -0
- package/jest.config.js +4 -0
- package/package.json +10 -4
- package/src/client.ts +36 -34
- package/src/config/constants.ts +3 -0
- package/src/index.ts +2 -1
- package/src/model/driftUser.ts +12 -2
- package/src/services/driftClientService.ts +37 -0
- package/src/tests/helpers.test.ts +48 -0
- package/src/user.ts +147 -5
- package/src/utils/helpers.ts +68 -0
- package/src/utils/jupiter.ts +73 -0
- package/src/helpers.ts +0 -33
|
@@ -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
|
+
}
|