@drift-labs/sdk 2.84.0-beta.5 → 2.84.0-beta.7

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.
@@ -7,6 +7,7 @@ import { QuoteAssetOracleClient } from '../oracles/quoteAssetOracleClient';
7
7
  import { BN, Program } from '@coral-xyz/anchor';
8
8
  import { PrelaunchOracleClient } from '../oracles/prelaunchOracleClient';
9
9
  import { SwitchboardClient } from '../oracles/switchboardClient';
10
+ import { PythPullClient } from '../oracles/pythPullClient';
10
11
 
11
12
  export function getOracleClient(
12
13
  oracleSource: OracleSource,
@@ -17,18 +18,34 @@ export function getOracleClient(
17
18
  return new PythClient(connection);
18
19
  }
19
20
 
21
+ if (isVariant(oracleSource, 'pythPull')) {
22
+ return new PythPullClient(connection);
23
+ }
24
+
20
25
  if (isVariant(oracleSource, 'pyth1K')) {
21
26
  return new PythClient(connection, new BN(1000));
22
27
  }
23
28
 
29
+ if (isVariant(oracleSource, 'pyth1KPull')) {
30
+ return new PythPullClient(connection, new BN(1000));
31
+ }
32
+
24
33
  if (isVariant(oracleSource, 'pyth1M')) {
25
34
  return new PythClient(connection, new BN(1000000));
26
35
  }
27
36
 
37
+ if (isVariant(oracleSource, 'pyth1MPull')) {
38
+ return new PythPullClient(connection, new BN(1000000));
39
+ }
40
+
28
41
  if (isVariant(oracleSource, 'pythStableCoin')) {
29
42
  return new PythClient(connection, undefined, true);
30
43
  }
31
44
 
45
+ if (isVariant(oracleSource, 'pythStableCoinPull')) {
46
+ return new PythPullClient(connection, undefined, true);
47
+ }
48
+
32
49
  if (isVariant(oracleSource, 'switchboard')) {
33
50
  return new SwitchboardClient(connection);
34
51
  }
@@ -9254,6 +9254,18 @@
9254
9254
  },
9255
9255
  {
9256
9256
  "name": "Prelaunch"
9257
+ },
9258
+ {
9259
+ "name": "PythPull"
9260
+ },
9261
+ {
9262
+ "name": "Pyth1KPull"
9263
+ },
9264
+ {
9265
+ "name": "Pyth1MPull"
9266
+ },
9267
+ {
9268
+ "name": "PythStableCoinPull"
9257
9269
  }
9258
9270
  ]
9259
9271
  }
@@ -11992,6 +12004,11 @@
11992
12004
  "code": 6266,
11993
12005
  "name": "OracleStaleForAMM",
11994
12006
  "msg": "OracleStaleForAMM"
12007
+ },
12008
+ {
12009
+ "code": 6267,
12010
+ "name": "UnableToParsePullOracleMessage",
12011
+ "msg": "Unable to parse pull oracle message"
11995
12012
  }
11996
12013
  ]
11997
12014
  }
@@ -363,6 +363,17 @@ export function calculateSpotMarketBorrowCapacity(
363
363
  remainingCapacity = BN.max(ZERO, totalCapacity.sub(tokenBorrowAmount));
364
364
  }
365
365
 
366
+ if (spotMarketAccount.maxTokenBorrowsFraction > 0) {
367
+ const maxTokenBorrows = spotMarketAccount.maxTokenDeposits
368
+ .mul(new BN(spotMarketAccount.maxTokenBorrowsFraction))
369
+ .divn(10000);
370
+
371
+ remainingCapacity = BN.min(
372
+ remainingCapacity,
373
+ BN.max(ZERO, maxTokenBorrows.sub(tokenBorrowAmount))
374
+ );
375
+ }
376
+
366
377
  return { totalCapacity, remainingCapacity };
367
378
  }
368
379
 
@@ -395,7 +406,10 @@ export function calculateInterestRate(
395
406
  .div(SPOT_MARKET_UTILIZATION_PRECISION);
396
407
  }
397
408
 
398
- return interestRate;
409
+ return BN.max(
410
+ interestRate,
411
+ new BN(bank.minBorrowRate).mul(PERCENTAGE_PRECISION.divn(200))
412
+ );
399
413
  }
400
414
 
401
415
  export function calculateDepositRate(
@@ -0,0 +1,112 @@
1
+ import { Connection, Keypair, PublicKey } from '@solana/web3.js';
2
+ import { OracleClient, OraclePriceData } from './types';
3
+ import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
4
+ import {
5
+ ONE,
6
+ PRICE_PRECISION,
7
+ QUOTE_PRECISION,
8
+ TEN,
9
+ } from '../constants/numericConstants';
10
+ import {
11
+ PythSolanaReceiverProgram,
12
+ DEFAULT_RECEIVER_PROGRAM_ID,
13
+ pythSolanaReceiverIdl,
14
+ } from '@pythnetwork/pyth-solana-receiver';
15
+ import { PriceUpdateAccount } from '@pythnetwork/pyth-solana-receiver/lib/PythSolanaReceiver';
16
+ import { Wallet } from '..';
17
+
18
+ export class PythPullClient implements OracleClient {
19
+ private connection: Connection;
20
+ private multiple: BN;
21
+ private stableCoin: boolean;
22
+ readonly receiver: Program<PythSolanaReceiverProgram>;
23
+ readonly decodeFunc: (name: string, data: Buffer) => PriceUpdateAccount;
24
+
25
+ public constructor(
26
+ connection: Connection,
27
+ multiple = ONE,
28
+ stableCoin = false
29
+ ) {
30
+ this.connection = connection;
31
+ this.multiple = multiple;
32
+ this.stableCoin = stableCoin;
33
+ const provider = new AnchorProvider(
34
+ this.connection,
35
+ //@ts-ignore
36
+ new Wallet(new Keypair()),
37
+ {
38
+ commitment: connection.commitment,
39
+ }
40
+ );
41
+ this.receiver = new Program<PythSolanaReceiverProgram>(
42
+ pythSolanaReceiverIdl as PythSolanaReceiverProgram,
43
+ DEFAULT_RECEIVER_PROGRAM_ID,
44
+ provider
45
+ );
46
+ this.decodeFunc =
47
+ this.receiver.account.priceUpdateV2.coder.accounts.decodeUnchecked.bind(
48
+ this.receiver.account.priceUpdateV2.coder.accounts
49
+ );
50
+ }
51
+
52
+ public async getOraclePriceData(
53
+ pricePublicKey: PublicKey
54
+ ): Promise<OraclePriceData> {
55
+ const accountInfo = await this.connection.getAccountInfo(pricePublicKey);
56
+ return this.getOraclePriceDataFromBuffer(accountInfo.data);
57
+ }
58
+
59
+ public getOraclePriceDataFromBuffer(buffer: Buffer): OraclePriceData {
60
+ const message = this.decodeFunc('priceUpdateV2', buffer);
61
+ const priceData = message.priceMessage;
62
+ const confidence = convertPythPrice(
63
+ priceData.conf,
64
+ priceData.exponent,
65
+ this.multiple
66
+ );
67
+ let price = convertPythPrice(
68
+ priceData.price,
69
+ priceData.exponent,
70
+ this.multiple
71
+ );
72
+ if (this.stableCoin) {
73
+ price = getStableCoinPrice(price, confidence);
74
+ }
75
+
76
+ return {
77
+ price,
78
+ slot: message.postedSlot,
79
+ confidence,
80
+ twap: convertPythPrice(
81
+ priceData.price,
82
+ priceData.exponent,
83
+ this.multiple
84
+ ),
85
+ twapConfidence: convertPythPrice(
86
+ priceData.price,
87
+ priceData.exponent,
88
+ this.multiple
89
+ ),
90
+ hasSufficientNumberOfDataPoints: true,
91
+ };
92
+ }
93
+ }
94
+
95
+ export function convertPythPrice(
96
+ price: BN,
97
+ exponent: number,
98
+ multiple: BN
99
+ ): BN {
100
+ exponent = Math.abs(exponent);
101
+ const pythPrecision = TEN.pow(new BN(exponent).abs()).div(multiple);
102
+ return price.mul(PRICE_PRECISION).div(pythPrecision);
103
+ }
104
+
105
+ const fiveBPS = new BN(500);
106
+ function getStableCoinPrice(price: BN, confidence: BN): BN {
107
+ if (price.sub(QUOTE_PRECISION).abs().lt(BN.min(confidence, fiveBPS))) {
108
+ return QUOTE_PRECISION;
109
+ } else {
110
+ return price;
111
+ }
112
+ }
@@ -20,6 +20,7 @@ import {
20
20
  BaseTxParams,
21
21
  DriftClientMetricsEvents,
22
22
  IWallet,
23
+ MappedRecord,
23
24
  SignedTxData,
24
25
  TxParams,
25
26
  } from '../types';
@@ -160,6 +161,10 @@ export class TxHandler {
160
161
  return (tx as VersionedTransaction)?.message && true;
161
162
  }
162
163
 
164
+ private isLegacyTransaction(tx: Transaction | VersionedTransaction) {
165
+ return !this.isVersionedTransaction(tx);
166
+ }
167
+
163
168
  private getTxSigFromSignedTx(signedTx: Transaction | VersionedTransaction) {
164
169
  if (this.isVersionedTransaction(signedTx)) {
165
170
  return bs58.encode(
@@ -266,7 +271,7 @@ export class TxHandler {
266
271
  return;
267
272
  }
268
273
 
269
- const fullTxData = txData.map((tx) => {
274
+ const signedTxData = txData.map((tx) => {
270
275
  const lastValidBlockHeight =
271
276
  this.blockHashToLastValidBlockHeightLookup[tx.blockHash];
272
277
 
@@ -277,8 +282,10 @@ export class TxHandler {
277
282
  });
278
283
 
279
284
  if (this.onSignedCb) {
280
- this.onSignedCb(fullTxData);
285
+ this.onSignedCb(signedTxData);
281
286
  }
287
+
288
+ return signedTxData;
282
289
  }
283
290
 
284
291
  /**
@@ -373,8 +380,15 @@ export class TxHandler {
373
380
  return tx;
374
381
  }
375
382
 
376
- public generateLegacyTransaction(ixs: TransactionInstruction[]) {
377
- return new Transaction().add(...ixs);
383
+ public generateLegacyTransaction(
384
+ ixs: TransactionInstruction[],
385
+ recentBlockhash?: BlockhashWithExpiryBlockHeight
386
+ ) {
387
+ const tx = new Transaction().add(...ixs);
388
+ if (recentBlockhash) {
389
+ tx.recentBlockhash = recentBlockhash.blockhash;
390
+ }
391
+ return tx;
378
392
  }
379
393
 
380
394
  /**
@@ -484,7 +498,7 @@ export class TxHandler {
484
498
  if (forceVersionedTransaction) {
485
499
  return this.generateLegacyVersionedTransaction(recentBlockhash, allIx);
486
500
  } else {
487
- return this.generateLegacyTransaction(allIx);
501
+ return this.generateLegacyTransaction(allIx, recentBlockhash);
488
502
  }
489
503
  } else {
490
504
  const marketLookupTable = await fetchMarketLookupTableAccount();
@@ -532,39 +546,6 @@ export class TxHandler {
532
546
  return tx.add(instruction);
533
547
  }
534
548
 
535
- /**
536
- * Build a map of transactions from an array of instructions for multiple transactions.
537
- * @param txsToSign
538
- * @param keys
539
- * @param wallet
540
- * @param commitment
541
- * @returns
542
- */
543
- public async buildTransactionMap(
544
- txsToSign: (Transaction | undefined)[],
545
- keys: string[],
546
- wallet?: IWallet,
547
- commitment?: Commitment,
548
- recentBlockhash?: BlockhashWithExpiryBlockHeight
549
- ) {
550
- recentBlockhash = recentBlockhash
551
- ? recentBlockhash
552
- : await this.getLatestBlockhashForTransaction();
553
-
554
- this.addHashAndExpiryToLookup(recentBlockhash);
555
-
556
- for (const tx of txsToSign) {
557
- if (!tx) continue;
558
- tx.recentBlockhash = recentBlockhash.blockhash;
559
- tx.feePayer = wallet?.publicKey ?? this.wallet?.publicKey;
560
-
561
- // @ts-ignore
562
- tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
563
- }
564
-
565
- return this.getSignedTransactionMap(txsToSign, keys, wallet);
566
- }
567
-
568
549
  /**
569
550
  * Get a map of signed and prepared transactions from an array of legacy transactions
570
551
  * @param txsToSign
@@ -573,9 +554,10 @@ export class TxHandler {
573
554
  * @param commitment
574
555
  * @returns
575
556
  */
576
- public async getPreparedAndSignedLegacyTransactionMap(
577
- txsToSign: (Transaction | undefined)[],
578
- keys: string[],
557
+ public async getPreparedAndSignedLegacyTransactionMap<
558
+ T extends Record<string, Transaction | undefined>,
559
+ >(
560
+ txsMap: T,
579
561
  wallet?: IWallet,
580
562
  commitment?: Commitment,
581
563
  recentBlockhash?: BlockhashWithExpiryBlockHeight
@@ -586,7 +568,7 @@ export class TxHandler {
586
568
 
587
569
  this.addHashAndExpiryToLookup(recentBlockhash);
588
570
 
589
- for (const tx of txsToSign) {
571
+ for (const tx of Object.values(txsMap)) {
590
572
  if (!tx) continue;
591
573
  tx.recentBlockhash = recentBlockhash.blockhash;
592
574
  tx.feePayer = wallet?.publicKey ?? this.wallet?.publicKey;
@@ -595,7 +577,7 @@ export class TxHandler {
595
577
  tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash;
596
578
  }
597
579
 
598
- return this.getSignedTransactionMap(txsToSign, keys, wallet);
580
+ return this.getSignedTransactionMap(txsMap, wallet);
599
581
  }
600
582
 
601
583
  /**
@@ -605,47 +587,49 @@ export class TxHandler {
605
587
  * @param wallet
606
588
  * @returns
607
589
  */
608
- public async getSignedTransactionMap(
609
- txsToSign: (Transaction | VersionedTransaction | undefined)[],
610
- keys: string[],
590
+ public async getSignedTransactionMap<
591
+ T extends Record<string, Transaction | VersionedTransaction | undefined>,
592
+ >(
593
+ txsToSignMap: T,
611
594
  wallet?: IWallet
612
595
  ): Promise<{
613
- [key: string]: Transaction | VersionedTransaction | undefined;
596
+ signedTxMap: T;
597
+ signedTxData: SignedTxData[];
614
598
  }> {
615
599
  [wallet] = this.getProps(wallet);
616
600
 
617
- const signedTxMap: {
618
- [key: string]: Transaction | VersionedTransaction | undefined;
619
- } = {};
601
+ const txsToSignEntries = Object.entries(txsToSignMap);
620
602
 
621
- const keysWithTx = [];
622
- txsToSign.forEach((tx, index) => {
623
- if (tx == undefined) {
624
- signedTxMap[keys[index]] = undefined;
625
- } else {
626
- keysWithTx.push(keys[index]);
603
+ // Create a map of the same keys as the input map, but with the values set to undefined. We'll populate the filtered (non-undefined) values with signed transactions.
604
+ const signedTxMap = txsToSignEntries.reduce((acc, [key]) => {
605
+ acc[key] = undefined;
606
+ return acc;
607
+ }, {}) as T;
608
+
609
+ const filteredTxEntries = txsToSignEntries.filter(([_, tx]) => !!tx);
610
+
611
+ // Extra handling for legacy transactions
612
+ for (const [_key, tx] of filteredTxEntries) {
613
+ if (this.isLegacyTransaction(tx)) {
614
+ (tx as Transaction).feePayer = wallet.publicKey;
627
615
  }
628
- });
616
+ }
629
617
 
630
618
  this.preSignedCb?.();
631
619
 
632
- const filteredTxs = txsToSign
633
- .map((tx) => {
634
- return tx as Transaction;
635
- })
636
- .filter((tx) => tx !== undefined);
637
-
638
- const signedTxs = await wallet.signAllTransactions(filteredTxs);
620
+ const signedFilteredTxs = await wallet.signAllTransactions(
621
+ filteredTxEntries.map(([_, tx]) => tx as Transaction)
622
+ );
639
623
 
640
- signedTxs.forEach((signedTx, index) => {
624
+ signedFilteredTxs.forEach((signedTx, index) => {
641
625
  // @ts-ignore
642
626
  signedTx.SIGNATURE_BLOCK_AND_EXPIRY =
643
627
  // @ts-ignore
644
- txsToSign[index]?.SIGNATURE_BLOCK_AND_EXPIRY;
628
+ filteredTxEntries[index][1]?.SIGNATURE_BLOCK_AND_EXPIRY;
645
629
  });
646
630
 
647
- this.handleSignedTxData(
648
- signedTxs.map((signedTx) => {
631
+ const signedTxData = this.handleSignedTxData(
632
+ signedFilteredTxs.map((signedTx) => {
649
633
  return {
650
634
  txSig: this.getTxSigFromSignedTx(signedTx),
651
635
  signedTx,
@@ -654,11 +638,36 @@ export class TxHandler {
654
638
  })
655
639
  );
656
640
 
657
- signedTxs.forEach((signedTx, index) => {
658
- signedTxMap[keysWithTx[index]] = signedTx;
641
+ filteredTxEntries.forEach(([key], index) => {
642
+ const signedTx = signedFilteredTxs[index];
643
+ // @ts-ignore
644
+ signedTxMap[key] = signedTx;
645
+ });
646
+
647
+ return { signedTxMap, signedTxData };
648
+ }
649
+
650
+ /**
651
+ * Accepts multiple instructions and builds a transaction for each. Prevents needing to spam RPC with requests for the same blockhash.
652
+ * @param props
653
+ * @returns
654
+ */
655
+ public async buildTransactionsMap<
656
+ T extends Record<string, TransactionInstruction | TransactionInstruction[]>,
657
+ >(
658
+ props: Omit<TxBuildingProps, 'instructions'> & {
659
+ instructionsMap: T;
660
+ }
661
+ ): Promise<MappedRecord<T, Transaction | VersionedTransaction>> {
662
+ const builtTxs = await this.buildBulkTransactions({
663
+ ...props,
664
+ instructions: Object.values(props.instructionsMap),
659
665
  });
660
666
 
661
- return signedTxMap;
667
+ return Object.keys(props.instructionsMap).reduce((acc, key, index) => {
668
+ acc[key] = builtTxs[index];
669
+ return acc;
670
+ }, {}) as MappedRecord<T, Transaction | VersionedTransaction>;
662
671
  }
663
672
 
664
673
  /**
@@ -666,22 +675,22 @@ export class TxHandler {
666
675
  * @param props
667
676
  * @returns
668
677
  */
669
- public async buildAndSignTransactionMap(
678
+ public async buildAndSignTransactionMap<
679
+ T extends Record<string, TransactionInstruction | TransactionInstruction[]>,
680
+ >(
670
681
  props: Omit<TxBuildingProps, 'instructions'> & {
671
- keys: string[];
672
- instructions: (TransactionInstruction | TransactionInstruction[])[];
682
+ instructionsMap: T;
673
683
  }
674
684
  ) {
675
- const transactions = await this.buildBulkTransactions(props);
685
+ const builtTxs = await this.buildTransactionsMap(props);
676
686
 
677
687
  const preppedTransactions = await (props.txVersion === 'legacy'
678
688
  ? this.getPreparedAndSignedLegacyTransactionMap(
679
- transactions as Transaction[],
680
- props.keys,
689
+ builtTxs as Record<string, Transaction>,
681
690
  props.wallet,
682
691
  props.preFlightCommitment
683
692
  )
684
- : this.getSignedTransactionMap(transactions, props.keys, props.wallet));
693
+ : this.getSignedTransactionMap(builtTxs, props.wallet));
685
694
 
686
695
  return preppedTransactions;
687
696
  }
package/src/types.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
2
2
  import { BN, ZERO } from '.';
3
3
 
4
+ // Utility type which lets you denote record with values of type A mapped to a record with the same keys but values of type B
5
+ export type MappedRecord<A extends Record<string, unknown>, B> = {
6
+ [K in keyof A]: B;
7
+ };
8
+
4
9
  // # Utility Types / Enums / Constants
5
10
 
6
11
  export enum ExchangeStatus {
@@ -103,9 +108,13 @@ export class OracleSource {
103
108
  static readonly PYTH = { pyth: {} };
104
109
  static readonly PYTH_1K = { pyth1K: {} };
105
110
  static readonly PYTH_1M = { pyth1M: {} };
111
+ static readonly PYTH_PULL = { pythPull: {} };
112
+ static readonly PYTH_1K_PULL = { pyth1KPull: {} };
113
+ static readonly PYTH_1M_PULL = { pyth1MPull: {} };
106
114
  static readonly SWITCHBOARD = { switchboard: {} };
107
115
  static readonly QUOTE_ASSET = { quoteAsset: {} };
108
116
  static readonly PYTH_STABLE_COIN = { pythStableCoin: {} };
117
+ static readonly PYTH_STABLE_COIN_PULL = { pythStableCoinPull: {} };
109
118
  static readonly Prelaunch = { prelaunch: {} };
110
119
  }
111
120
 
@@ -722,6 +731,9 @@ export type SpotMarketAccount = {
722
731
  pausedOperations: number;
723
732
 
724
733
  ifPausedOperations: number;
734
+
735
+ maxTokenBorrowsFraction: number;
736
+ minBorrowRate: number;
725
737
  };
726
738
 
727
739
  export type PoolBalance = {