@drift-labs/sdk 2.31.1-beta.2 → 2.31.1-beta.21

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 (51) hide show
  1. package/VERSION +1 -1
  2. package/lib/accounts/mockUserAccountSubscriber.d.ts +23 -0
  3. package/lib/accounts/mockUserAccountSubscriber.js +31 -0
  4. package/lib/constants/perpMarkets.js +20 -0
  5. package/lib/dlob/orderBookLevels.js +2 -2
  6. package/lib/driftClient.d.ts +57 -4
  7. package/lib/driftClient.js +244 -205
  8. package/lib/driftClientConfig.d.ts +2 -1
  9. package/lib/idl/drift.json +31 -1
  10. package/lib/index.d.ts +2 -0
  11. package/lib/index.js +2 -0
  12. package/lib/marinade/index.d.ts +11 -0
  13. package/lib/marinade/index.js +36 -0
  14. package/lib/marinade/types.d.ts +1963 -0
  15. package/lib/marinade/types.js +1965 -0
  16. package/lib/math/spotBalance.d.ts +9 -2
  17. package/lib/math/spotBalance.js +54 -6
  18. package/lib/math/superStake.d.ts +22 -0
  19. package/lib/math/superStake.js +108 -0
  20. package/lib/math/tiers.d.ts +4 -0
  21. package/lib/math/tiers.js +52 -0
  22. package/lib/tx/retryTxSender.d.ts +12 -3
  23. package/lib/tx/retryTxSender.js +22 -22
  24. package/lib/tx/types.d.ts +2 -2
  25. package/lib/user.d.ts +10 -1
  26. package/lib/user.js +39 -8
  27. package/lib/userConfig.d.ts +4 -0
  28. package/lib/userStats.js +4 -1
  29. package/lib/userStatsConfig.d.ts +2 -0
  30. package/package.json +1 -1
  31. package/src/accounts/mockUserAccountSubscriber.ts +53 -0
  32. package/src/config.ts +2 -2
  33. package/src/constants/perpMarkets.ts +20 -0
  34. package/src/dlob/orderBookLevels.ts +3 -2
  35. package/src/driftClient.ts +440 -224
  36. package/src/driftClientConfig.ts +2 -1
  37. package/src/idl/drift.json +31 -1
  38. package/src/index.ts +2 -0
  39. package/src/marinade/idl/idl.json +1962 -0
  40. package/src/marinade/index.ts +64 -0
  41. package/src/marinade/types.ts +3925 -0
  42. package/src/math/spotBalance.ts +83 -5
  43. package/src/math/superStake.ts +148 -0
  44. package/src/math/tiers.ts +44 -0
  45. package/src/tx/retryTxSender.ts +39 -35
  46. package/src/tx/types.ts +2 -2
  47. package/src/user.ts +63 -12
  48. package/src/userConfig.ts +5 -0
  49. package/src/userStats.ts +4 -0
  50. package/src/userStatsConfig.ts +3 -0
  51. package/tests/spot/test.ts +156 -0
@@ -236,18 +236,27 @@ export function calculateLiabilityWeight(
236
236
  return liabilityWeight;
237
237
  }
238
238
 
239
- export function calculateUtilization(bank: SpotMarketAccount): BN {
240
- const tokenDepositAmount = getTokenAmount(
239
+ export function calculateUtilization(
240
+ bank: SpotMarketAccount,
241
+ delta = ZERO
242
+ ): BN {
243
+ let tokenDepositAmount = getTokenAmount(
241
244
  bank.depositBalance,
242
245
  bank,
243
246
  SpotBalanceType.DEPOSIT
244
247
  );
245
- const tokenBorrowAmount = getTokenAmount(
248
+ let tokenBorrowAmount = getTokenAmount(
246
249
  bank.borrowBalance,
247
250
  bank,
248
251
  SpotBalanceType.BORROW
249
252
  );
250
253
 
254
+ if (delta.gt(ZERO)) {
255
+ tokenDepositAmount = tokenDepositAmount.add(delta);
256
+ } else if (delta.lt(ZERO)) {
257
+ tokenBorrowAmount = tokenBorrowAmount.add(delta.abs());
258
+ }
259
+
251
260
  let utilization: BN;
252
261
  if (tokenBorrowAmount.eq(ZERO) && tokenDepositAmount.eq(ZERO)) {
253
262
  utilization = ZERO;
@@ -262,9 +271,78 @@ export function calculateUtilization(bank: SpotMarketAccount): BN {
262
271
  return utilization;
263
272
  }
264
273
 
265
- export function calculateInterestRate(bank: SpotMarketAccount): BN {
266
- const utilization = calculateUtilization(bank);
274
+ /**
275
+ * calculates max borrow amount where rate would stay below targetBorrowRate
276
+ * @param spotMarketAccount
277
+ * @param targetBorrowRate
278
+ * @returns : Precision: TOKEN DECIMALS
279
+ */
280
+ export function calculateSpotMarketBorrowCapacity(
281
+ spotMarketAccount: SpotMarketAccount,
282
+ targetBorrowRate: BN
283
+ ): BN {
284
+ const currentBorrowRate = calculateBorrowRate(spotMarketAccount);
267
285
 
286
+ if (currentBorrowRate.gte(targetBorrowRate)) {
287
+ return ZERO;
288
+ } else {
289
+ const tokenDepositAmount = getTokenAmount(
290
+ spotMarketAccount.depositBalance,
291
+ spotMarketAccount,
292
+ SpotBalanceType.DEPOSIT
293
+ );
294
+ const tokenBorrowAmount = getTokenAmount(
295
+ spotMarketAccount.borrowBalance,
296
+ spotMarketAccount,
297
+ SpotBalanceType.BORROW
298
+ );
299
+
300
+ let targetUtilization;
301
+
302
+ // target utilization past mid point
303
+ if (targetBorrowRate.gte(new BN(spotMarketAccount.optimalBorrowRate))) {
304
+ const borrowRateSlope = new BN(
305
+ spotMarketAccount.maxBorrowRate - spotMarketAccount.optimalBorrowRate
306
+ )
307
+ .mul(SPOT_MARKET_UTILIZATION_PRECISION)
308
+ .div(
309
+ SPOT_MARKET_UTILIZATION_PRECISION.sub(
310
+ new BN(spotMarketAccount.optimalUtilization)
311
+ )
312
+ );
313
+
314
+ const surplusTargetUtilization = targetBorrowRate
315
+ .sub(new BN(spotMarketAccount.optimalBorrowRate))
316
+ .mul(SPOT_MARKET_UTILIZATION_PRECISION)
317
+ .div(borrowRateSlope);
318
+
319
+ targetUtilization = surplusTargetUtilization.add(
320
+ new BN(spotMarketAccount.optimalUtilization)
321
+ );
322
+ } else {
323
+ const borrowRateSlope = new BN(spotMarketAccount.optimalBorrowRate)
324
+ .mul(SPOT_MARKET_UTILIZATION_PRECISION)
325
+ .div(new BN(spotMarketAccount.optimalUtilization));
326
+
327
+ targetUtilization = targetBorrowRate
328
+ .mul(SPOT_MARKET_UTILIZATION_PRECISION)
329
+ .div(borrowRateSlope);
330
+ }
331
+
332
+ const targetBorrowAmount = tokenDepositAmount
333
+ .mul(targetUtilization)
334
+ .div(SPOT_MARKET_UTILIZATION_PRECISION);
335
+ const capacity = BN.max(ZERO, targetBorrowAmount.sub(tokenBorrowAmount));
336
+
337
+ return capacity;
338
+ }
339
+ }
340
+
341
+ export function calculateInterestRate(
342
+ bank: SpotMarketAccount,
343
+ delta = ZERO
344
+ ): BN {
345
+ const utilization = calculateUtilization(bank, delta);
268
346
  let interestRate: BN;
269
347
  if (utilization.gt(new BN(bank.optimalUtilization))) {
270
348
  const surplusUtilization = utilization.sub(new BN(bank.optimalUtilization));
@@ -0,0 +1,148 @@
1
+ import {
2
+ AddressLookupTableAccount,
3
+ LAMPORTS_PER_SOL,
4
+ PublicKey,
5
+ TransactionInstruction,
6
+ } from '@solana/web3.js';
7
+ import { JupiterClient } from '../jupiter/jupiterClient';
8
+ import { DriftClient } from '../driftClient';
9
+ import { getMarinadeFinanceProgram, getMarinadeMSolPrice } from '../marinade';
10
+ import { BN } from '@coral-xyz/anchor';
11
+ import { User } from '../user';
12
+ import { DepositRecord, isVariant } from '../types';
13
+ import { LAMPORTS_PRECISION, ZERO } from '../constants/numericConstants';
14
+ import fetch from 'node-fetch';
15
+
16
+ export async function findBestSuperStakeIxs({
17
+ amount,
18
+ jupiterClient,
19
+ driftClient,
20
+ userAccountPublicKey,
21
+ }: {
22
+ amount: BN;
23
+ jupiterClient: JupiterClient;
24
+ driftClient: DriftClient;
25
+ userAccountPublicKey?: PublicKey;
26
+ }): Promise<{
27
+ ixs: TransactionInstruction[];
28
+ lookupTables: AddressLookupTableAccount[];
29
+ method: 'jupiter' | 'marinade';
30
+ price: number;
31
+ }> {
32
+ const marinadeProgram = getMarinadeFinanceProgram(driftClient.provider);
33
+ const marinadePrice = await getMarinadeMSolPrice(marinadeProgram);
34
+
35
+ const solMint = driftClient.getSpotMarketAccount(1).mint;
36
+ const mSOLMint = driftClient.getSpotMarketAccount(2).mint;
37
+ const jupiterRoutes = await jupiterClient.getRoutes({
38
+ inputMint: solMint,
39
+ outputMint: mSOLMint,
40
+ amount,
41
+ });
42
+
43
+ const bestRoute = jupiterRoutes[0];
44
+ const jupiterPrice = bestRoute.inAmount / bestRoute.outAmount;
45
+
46
+ if (marinadePrice <= jupiterPrice) {
47
+ const ixs = await driftClient.getStakeForMSOLIx({ amount });
48
+ return {
49
+ method: 'marinade',
50
+ ixs,
51
+ lookupTables: [],
52
+ price: marinadePrice,
53
+ };
54
+ } else {
55
+ const { ixs, lookupTables } = await driftClient.getJupiterSwapIx({
56
+ inMarketIndex: 1,
57
+ outMarketIndex: 2,
58
+ route: bestRoute,
59
+ jupiterClient,
60
+ amount,
61
+ userAccountPublicKey,
62
+ });
63
+ return {
64
+ method: 'jupiter',
65
+ ixs,
66
+ lookupTables,
67
+ price: jupiterPrice,
68
+ };
69
+ }
70
+ }
71
+
72
+ export async function calculateSolEarned({
73
+ user,
74
+ depositRecords,
75
+ }: {
76
+ user: User;
77
+ depositRecords: DepositRecord[];
78
+ }): Promise<BN> {
79
+ const now = Date.now() / 1000;
80
+ const timestamps: number[] = [
81
+ now,
82
+ ...depositRecords.map((r) => r.ts.toNumber()),
83
+ ];
84
+
85
+ const msolRatios = new Map<number, number>();
86
+
87
+ const getPrice = async (timestamp) => {
88
+ const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds
89
+ const swaggerApiDateTime = date.toISOString(); // Format date as swagger API date-time
90
+ const url = `https://api.marinade.finance/msol/price_sol?time=${swaggerApiDateTime}`;
91
+ const response = await fetch(url);
92
+ if (response.status === 200) {
93
+ const data = await response.json();
94
+ msolRatios.set(timestamp, data);
95
+ }
96
+ };
97
+
98
+ await Promise.all(timestamps.map(getPrice));
99
+
100
+ let solEarned = ZERO;
101
+ for (const record of depositRecords) {
102
+ if (record.marketIndex === 1) {
103
+ if (isVariant(record.direction, 'deposit')) {
104
+ solEarned = solEarned.sub(record.amount);
105
+ } else {
106
+ solEarned = solEarned.add(record.amount);
107
+ }
108
+ } else if (record.marketIndex === 2) {
109
+ const msolRatio = msolRatios.get(record.ts.toNumber());
110
+ const msolRatioBN = new BN(msolRatio * LAMPORTS_PER_SOL);
111
+
112
+ const solAmount = record.amount.mul(msolRatioBN).div(LAMPORTS_PRECISION);
113
+ if (isVariant(record.direction, 'deposit')) {
114
+ solEarned = solEarned.sub(solAmount);
115
+ } else {
116
+ solEarned = solEarned.add(solAmount);
117
+ }
118
+ }
119
+ }
120
+
121
+ const currentMSOLTokenAmount = await user.getTokenAmount(2);
122
+ const currentSOLTokenAmount = await user.getTokenAmount(1);
123
+
124
+ const currentMSOLRatio = msolRatios.get(now);
125
+ const currentMSOLRatioBN = new BN(currentMSOLRatio * LAMPORTS_PER_SOL);
126
+
127
+ solEarned = solEarned.add(
128
+ currentMSOLTokenAmount.mul(currentMSOLRatioBN).div(LAMPORTS_PRECISION)
129
+ );
130
+ solEarned = solEarned.add(currentSOLTokenAmount);
131
+
132
+ return solEarned;
133
+ }
134
+
135
+ // calculate estimated liquidation price (in mSOL/SOL) based on target amounts
136
+ export function calculateEstimatedSuperStakeLiquidationPrice(
137
+ msolDepositAmount: number,
138
+ msolMaintenanceAssetWeight: number,
139
+ solBorrowAmount: number,
140
+ solMaintenanceLiabilityWeight: number,
141
+ msolPriceRatio: number
142
+ ): number {
143
+ const liquidationDivergence =
144
+ (solMaintenanceLiabilityWeight * solBorrowAmount) /
145
+ (msolMaintenanceAssetWeight * msolDepositAmount * msolPriceRatio);
146
+ const liquidationPrice = msolPriceRatio * liquidationDivergence;
147
+ return liquidationPrice;
148
+ }
@@ -0,0 +1,44 @@
1
+ import { isVariant, PerpMarketAccount, SpotMarketAccount } from '../types';
2
+
3
+ export function getPerpMarketTierNumber(perpMarket: PerpMarketAccount): number {
4
+ if (isVariant(perpMarket.contractTier, 'a')) {
5
+ return 0;
6
+ } else if (isVariant(perpMarket.contractTier, 'b')) {
7
+ return 1;
8
+ } else if (isVariant(perpMarket.contractTier, 'c')) {
9
+ return 2;
10
+ } else if (isVariant(perpMarket.contractTier, 'speculative')) {
11
+ return 3;
12
+ } else if (isVariant(perpMarket.contractTier, 'isolated')) {
13
+ return 4;
14
+ } else {
15
+ return 5;
16
+ }
17
+ }
18
+
19
+ export function getSpotMarketTierNumber(spotMarket: SpotMarketAccount): number {
20
+ if (isVariant(spotMarket.assetTier, 'collateral')) {
21
+ return 0;
22
+ } else if (isVariant(spotMarket.assetTier, 'protected')) {
23
+ return 1;
24
+ } else if (isVariant(spotMarket.assetTier, 'cross')) {
25
+ return 2;
26
+ } else if (isVariant(spotMarket.assetTier, 'isolated')) {
27
+ return 3;
28
+ } else if (isVariant(spotMarket.assetTier, 'unlisted')) {
29
+ return 4;
30
+ } else {
31
+ return 5;
32
+ }
33
+ }
34
+
35
+ export function perpTierIsAsSafeAs(
36
+ perpTier: number,
37
+ otherPerpTier: number,
38
+ otherSpotTier: number
39
+ ): boolean {
40
+ const asSafeAsPerp = perpTier <= otherPerpTier;
41
+ const asSafeAsSpot =
42
+ otherSpotTier === 4 || (otherSpotTier >= 2 && perpTier <= 2);
43
+ return asSafeAsSpot && asSafeAsPerp;
44
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import { AnchorProvider } from '@coral-xyz/anchor';
18
18
  import assert from 'assert';
19
19
  import bs58 from 'bs58';
20
+ import { IWallet } from '../types';
20
21
 
21
22
  const DEFAULT_TIMEOUT = 35000;
22
23
  const DEFAULT_RETRY = 8000;
@@ -26,21 +27,34 @@ type ResolveReference = {
26
27
  };
27
28
 
28
29
  export class RetryTxSender implements TxSender {
29
- provider: AnchorProvider;
30
+ connection: Connection;
31
+ wallet: IWallet;
32
+ opts: ConfirmOptions;
30
33
  timeout: number;
31
34
  retrySleep: number;
32
35
  additionalConnections: Connection[];
33
36
  timoutCount = 0;
34
37
 
35
- public constructor(
36
- provider: AnchorProvider,
37
- timeout?: number,
38
- retrySleep?: number,
39
- additionalConnections = new Array<Connection>()
40
- ) {
41
- this.provider = provider;
42
- this.timeout = timeout ?? DEFAULT_TIMEOUT;
43
- this.retrySleep = retrySleep ?? DEFAULT_RETRY;
38
+ public constructor({
39
+ connection,
40
+ wallet,
41
+ opts = AnchorProvider.defaultOptions(),
42
+ timeout = DEFAULT_TIMEOUT,
43
+ retrySleep = DEFAULT_RETRY,
44
+ additionalConnections = new Array<Connection>(),
45
+ }: {
46
+ connection: Connection;
47
+ wallet: IWallet;
48
+ opts?: ConfirmOptions;
49
+ timeout?: number;
50
+ retrySleep?: number;
51
+ additionalConnections?;
52
+ }) {
53
+ this.connection = connection;
54
+ this.wallet = wallet;
55
+ this.opts = opts;
56
+ this.timeout = timeout;
57
+ this.retrySleep = retrySleep;
44
58
  this.additionalConnections = additionalConnections;
45
59
  }
46
60
 
@@ -54,7 +68,7 @@ export class RetryTxSender implements TxSender {
54
68
  additionalSigners = [];
55
69
  }
56
70
  if (opts === undefined) {
57
- opts = this.provider.opts;
71
+ opts = this.opts;
58
72
  }
59
73
 
60
74
  const signedTx = preSigned
@@ -69,11 +83,9 @@ export class RetryTxSender implements TxSender {
69
83
  additionalSigners: Array<Signer>,
70
84
  opts: ConfirmOptions
71
85
  ): Promise<Transaction> {
72
- tx.feePayer = this.provider.wallet.publicKey;
86
+ tx.feePayer = this.wallet.publicKey;
73
87
  tx.recentBlockhash = (
74
- await this.provider.connection.getRecentBlockhash(
75
- opts.preflightCommitment
76
- )
88
+ await this.connection.getRecentBlockhash(opts.preflightCommitment)
77
89
  ).blockhash;
78
90
 
79
91
  additionalSigners
@@ -82,7 +94,7 @@ export class RetryTxSender implements TxSender {
82
94
  tx.partialSign(kp);
83
95
  });
84
96
 
85
- const signedTx = await this.provider.wallet.signTransaction(tx);
97
+ const signedTx = await this.wallet.signTransaction(tx);
86
98
 
87
99
  return signedTx;
88
100
  }
@@ -97,15 +109,13 @@ export class RetryTxSender implements TxSender {
97
109
  additionalSigners = [];
98
110
  }
99
111
  if (opts === undefined) {
100
- opts = this.provider.opts;
112
+ opts = this.opts;
101
113
  }
102
114
 
103
115
  const message = new TransactionMessage({
104
- payerKey: this.provider.wallet.publicKey,
116
+ payerKey: this.wallet.publicKey,
105
117
  recentBlockhash: (
106
- await this.provider.connection.getRecentBlockhash(
107
- opts.preflightCommitment
108
- )
118
+ await this.connection.getRecentBlockhash(opts.preflightCommitment)
109
119
  ).blockhash,
110
120
  instructions: ixs,
111
121
  }).compileToV0Message(lookupTableAccounts);
@@ -125,9 +135,9 @@ export class RetryTxSender implements TxSender {
125
135
  if (preSigned) {
126
136
  signedTx = tx;
127
137
  // @ts-ignore
128
- } else if (this.provider.wallet.payer) {
138
+ } else if (this.wallet.payer) {
129
139
  // @ts-ignore
130
- tx.sign((additionalSigners ?? []).concat(this.provider.wallet.payer));
140
+ tx.sign((additionalSigners ?? []).concat(this.wallet.payer));
131
141
  signedTx = tx;
132
142
  } else {
133
143
  additionalSigners
@@ -136,11 +146,11 @@ export class RetryTxSender implements TxSender {
136
146
  tx.sign([kp]);
137
147
  });
138
148
  // @ts-ignore
139
- signedTx = await this.provider.wallet.signTransaction(tx);
149
+ signedTx = await this.wallet.signTransaction(tx);
140
150
  }
141
151
 
142
152
  if (opts === undefined) {
143
- opts = this.provider.opts;
153
+ opts = this.opts;
144
154
  }
145
155
 
146
156
  return this.sendRawTransaction(signedTx.serialize(), opts);
@@ -154,10 +164,7 @@ export class RetryTxSender implements TxSender {
154
164
 
155
165
  let txid: TransactionSignature;
156
166
  try {
157
- txid = await this.provider.connection.sendRawTransaction(
158
- rawTransaction,
159
- opts
160
- );
167
+ txid = await this.connection.sendRawTransaction(rawTransaction, opts);
161
168
  this.sendToAdditionalConnections(rawTransaction, opts);
162
169
  } catch (e) {
163
170
  console.error(e);
@@ -179,7 +186,7 @@ export class RetryTxSender implements TxSender {
179
186
  while (!done && this.getTimestamp() - startTime < this.timeout) {
180
187
  await this.sleep(resolveReference);
181
188
  if (!done) {
182
- this.provider.connection
189
+ this.connection
183
190
  .sendRawTransaction(rawTransaction, opts)
184
191
  .catch((e) => {
185
192
  console.error(e);
@@ -218,13 +225,10 @@ export class RetryTxSender implements TxSender {
218
225
  assert(decodedSignature.length === 64, 'signature has invalid length');
219
226
 
220
227
  const start = Date.now();
221
- const subscriptionCommitment = commitment || this.provider.opts.commitment;
228
+ const subscriptionCommitment = commitment || this.opts.commitment;
222
229
 
223
230
  const subscriptionIds = new Array<number>();
224
- const connections = [
225
- this.provider.connection,
226
- ...this.additionalConnections,
227
- ];
231
+ const connections = [this.connection, ...this.additionalConnections];
228
232
  let response: RpcResponseAndContext<SignatureResult> | null = null;
229
233
  const promises = connections.map((connection, i) => {
230
234
  let subscriptionId;
package/src/tx/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { Provider } from '@coral-xyz/anchor';
2
1
  import {
3
2
  AddressLookupTableAccount,
4
3
  ConfirmOptions,
@@ -8,6 +7,7 @@ import {
8
7
  TransactionSignature,
9
8
  VersionedTransaction,
10
9
  } from '@solana/web3.js';
10
+ import { IWallet } from '../types';
11
11
 
12
12
  export type TxSigAndSlot = {
13
13
  txSig: TransactionSignature;
@@ -15,7 +15,7 @@ export type TxSigAndSlot = {
15
15
  };
16
16
 
17
17
  export interface TxSender {
18
- provider: Provider;
18
+ wallet: IWallet;
19
19
 
20
20
  send(
21
21
  tx: Transaction,
package/src/user.ts CHANGED
@@ -72,6 +72,7 @@ import {
72
72
  } from './math/spotPosition';
73
73
 
74
74
  import { calculateLiveOracleTwap } from './math/oracles';
75
+ import { getPerpMarketTierNumber, getSpotMarketTierNumber } from './math/tiers';
75
76
 
76
77
  export class User {
77
78
  driftClient: DriftClient;
@@ -97,6 +98,8 @@ export class User {
97
98
  config.userAccountPublicKey,
98
99
  config.accountSubscription.accountLoader
99
100
  );
101
+ } else if (config.accountSubscription?.type === 'custom') {
102
+ this.accountSubscriber = config.accountSubscription.userAccountSubscriber;
100
103
  } else {
101
104
  this.accountSubscriber = new WebSocketUserAccountSubscriber(
102
105
  config.driftClient.program,
@@ -301,7 +304,10 @@ export class User {
301
304
  marketIndex: number,
302
305
  originalPosition?: PerpPosition
303
306
  ): [PerpPosition, BN, BN] {
304
- originalPosition = originalPosition ?? this.getPerpPosition(marketIndex);
307
+ originalPosition =
308
+ originalPosition ??
309
+ this.getPerpPosition(marketIndex) ??
310
+ this.getEmptyPosition(marketIndex);
305
311
 
306
312
  if (originalPosition.lpShares.eq(ZERO)) {
307
313
  return [originalPosition, ZERO, ZERO];
@@ -368,7 +374,7 @@ export class User {
368
374
  let pnl;
369
375
  if (updateType == 'open' || updateType == 'increase') {
370
376
  newQuoteEntry = position.quoteEntryAmount.add(deltaQaa);
371
- pnl = 0;
377
+ pnl = ZERO;
372
378
  } else if (updateType == 'reduce' || updateType == 'close') {
373
379
  newQuoteEntry = position.quoteEntryAmount.sub(
374
380
  position.quoteEntryAmount
@@ -490,6 +496,12 @@ export class User {
490
496
  );
491
497
  }
492
498
 
499
+ public getActiveSpotPositions(): SpotPosition[] {
500
+ return this.getUserAccount().spotPositions.filter(
501
+ (pos) => !isSpotPositionAvailable(pos)
502
+ );
503
+ }
504
+
493
505
  /**
494
506
  * calculates unrealized position price pnl
495
507
  * @returns : Precision QUOTE_PRECISION
@@ -1127,7 +1139,8 @@ export class User {
1127
1139
  includeOpenOrders = false
1128
1140
  ): BN {
1129
1141
  const userPosition =
1130
- this.getPerpPosition(marketIndex) || this.getEmptyPosition(marketIndex);
1142
+ this.getPerpPositionWithLPSettle(marketIndex)[0] ||
1143
+ this.getEmptyPosition(marketIndex);
1131
1144
  const market = this.driftClient.getPerpMarketAccount(
1132
1145
  userPosition.marketIndex
1133
1146
  );
@@ -1435,24 +1448,29 @@ export class User {
1435
1448
  return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue);
1436
1449
  }
1437
1450
 
1438
- public canBeLiquidated(): boolean {
1451
+ public canBeLiquidated(): {
1452
+ canBeLiquidated: boolean;
1453
+ marginRequirement: BN;
1454
+ totalCollateral: BN;
1455
+ } {
1439
1456
  const totalCollateral = this.getTotalCollateral('Maintenance');
1440
1457
 
1441
1458
  // if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer
1442
1459
  let liquidationBuffer = undefined;
1443
- const isBeingLiquidated = isVariant(
1444
- this.getUserAccount().status,
1445
- 'beingLiquidated'
1446
- );
1447
-
1448
- if (isBeingLiquidated) {
1460
+ if (this.isBeingLiquidated()) {
1449
1461
  liquidationBuffer = new BN(
1450
1462
  this.driftClient.getStateAccount().liquidationMarginBufferRatio
1451
1463
  );
1452
1464
  }
1453
- const maintenanceRequirement =
1465
+ const marginRequirement =
1454
1466
  this.getMaintenanceMarginRequirement(liquidationBuffer);
1455
- return totalCollateral.lt(maintenanceRequirement);
1467
+ const canBeLiquidated = totalCollateral.lt(marginRequirement);
1468
+
1469
+ return {
1470
+ canBeLiquidated,
1471
+ marginRequirement,
1472
+ totalCollateral,
1473
+ };
1456
1474
  }
1457
1475
 
1458
1476
  public isBeingLiquidated(): boolean {
@@ -2304,6 +2322,38 @@ export class User {
2304
2322
  return true;
2305
2323
  }
2306
2324
 
2325
+ public getSafestTiers(): { perpTier: number; spotTier: number } {
2326
+ let safestPerpTier = 4;
2327
+ let safestSpotTier = 4;
2328
+
2329
+ for (const perpPosition of this.getActivePerpPositions()) {
2330
+ safestPerpTier = Math.min(
2331
+ safestPerpTier,
2332
+ getPerpMarketTierNumber(
2333
+ this.driftClient.getPerpMarketAccount(perpPosition.marketIndex)
2334
+ )
2335
+ );
2336
+ }
2337
+
2338
+ for (const spotPosition of this.getActiveSpotPositions()) {
2339
+ if (isVariant(spotPosition.balanceType, 'deposit')) {
2340
+ continue;
2341
+ }
2342
+
2343
+ safestSpotTier = Math.min(
2344
+ safestSpotTier,
2345
+ getSpotMarketTierNumber(
2346
+ this.driftClient.getSpotMarketAccount(spotPosition.marketIndex)
2347
+ )
2348
+ );
2349
+ }
2350
+
2351
+ return {
2352
+ perpTier: safestPerpTier,
2353
+ spotTier: safestSpotTier,
2354
+ };
2355
+ }
2356
+
2307
2357
  /**
2308
2358
  * Get the total position value, excluding any position coming from the given target market
2309
2359
  * @param marketToIgnore
@@ -2345,6 +2395,7 @@ export class User {
2345
2395
 
2346
2396
  return oracleData;
2347
2397
  }
2398
+
2348
2399
  private getOracleDataForSpotMarket(marketIndex: number): OraclePriceData {
2349
2400
  const oracleKey = this.driftClient.getSpotMarketAccount(marketIndex).oracle;
2350
2401
 
package/src/userConfig.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DriftClient } from './driftClient';
2
2
  import { PublicKey } from '@solana/web3.js';
3
3
  import { BulkAccountLoader } from './accounts/bulkAccountLoader';
4
+ import { UserAccountSubscriber } from './accounts/types';
4
5
 
5
6
  export type UserConfig = {
6
7
  accountSubscription?: UserSubscriptionConfig;
@@ -15,4 +16,8 @@ export type UserSubscriptionConfig =
15
16
  | {
16
17
  type: 'polling';
17
18
  accountLoader: BulkAccountLoader;
19
+ }
20
+ | {
21
+ type: 'custom';
22
+ userAccountSubscriber: UserAccountSubscriber;
18
23
  };
package/src/userStats.ts CHANGED
@@ -25,6 +25,10 @@ export class UserStats {
25
25
  config.userStatsAccountPublicKey,
26
26
  config.accountSubscription.accountLoader
27
27
  );
28
+ } else if (config.accountSubscription?.type === 'custom') {
29
+ throw new Error(
30
+ 'Custom account subscription not yet implemented for user stats'
31
+ );
28
32
  } else {
29
33
  this.accountSubscriber = new WebSocketUserStatsAccountSubscriber(
30
34
  config.driftClient.program,
@@ -15,4 +15,7 @@ export type UserStatsSubscriptionConfig =
15
15
  | {
16
16
  type: 'polling';
17
17
  accountLoader: BulkAccountLoader;
18
+ }
19
+ | {
20
+ type: 'custom';
18
21
  };