@drift-labs/sdk 2.85.0-beta.10 → 2.85.0-beta.11

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.85.0-beta.10
1
+ 2.85.0-beta.11
package/bun.lockb CHANGED
Binary file
@@ -221,6 +221,7 @@ export declare class DriftClient {
221
221
  */
222
222
  getAssociatedTokenAccount(marketIndex: number, useNative?: boolean): Promise<PublicKey>;
223
223
  createAssociatedTokenAccountIdempotentInstruction(account: PublicKey, payer: PublicKey, owner: PublicKey, mint: PublicKey): TransactionInstruction;
224
+ createDepositTxn(amount: BN, marketIndex: number, associatedTokenAccount: PublicKey, subAccountId?: number, reduceOnly?: boolean, txParams?: TxParams): Promise<ReturnType<typeof this.buildTransaction>>;
224
225
  /**
225
226
  * Deposit funds into the given spot market
226
227
  *
@@ -240,6 +241,7 @@ export declare class DriftClient {
240
241
  pubkey: PublicKey;
241
242
  }>;
242
243
  getAssociatedTokenAccountCreationIx(tokenMintAddress: PublicKey, associatedTokenAddress: PublicKey): anchor.web3.TransactionInstruction;
244
+ createInitializeUserAccountAndDepositCollateral(amount: BN, userTokenAccount: PublicKey, marketIndex?: number, subAccountId?: number, name?: string, fromSubAccountId?: number, referrerInfo?: ReferrerInfo, donateAmount?: BN, txParams?: TxParams, customMaxMarginRatio?: number): Promise<[Transaction | VersionedTransaction, PublicKey]>;
243
245
  /**
244
246
  * Creates the User account for a user, and deposits some initial collateral
245
247
  * @param amount
@@ -58,6 +58,7 @@ const utils_1 = require("./math/utils");
58
58
  const txParamProcessor_1 = require("./tx/txParamProcessor");
59
59
  const oracles_1 = require("./math/oracles");
60
60
  const txHandler_1 = require("./tx/txHandler");
61
+ const utils_2 = require("./tx/utils");
61
62
  /**
62
63
  * # DriftClient
63
64
  * This class is the main way to interact with Drift Protocol. It allows you to subscribe to the various accounts where the Market's state is stored, as well as: opening positions, liquidating, settling funding, depositing & withdrawing, and more.
@@ -106,6 +107,7 @@ class DriftClient {
106
107
  onSignedCb: this.handleSignedTransaction.bind(this),
107
108
  preSignedCb: this.handlePreSignedTransaction.bind(this),
108
109
  },
110
+ config: config.txHandlerConfig,
109
111
  });
110
112
  if (config.includeDelegates && config.subAccountIds) {
111
113
  throw new Error('Can only pass one of includeDelegates or subAccountIds. If you want to specify subaccount ids for multiple authorities, pass authoritySubaccountMap instead');
@@ -1061,17 +1063,7 @@ class DriftClient {
1061
1063
  data: Buffer.from([0x1]),
1062
1064
  });
1063
1065
  }
1064
- /**
1065
- * Deposit funds into the given spot market
1066
- *
1067
- * @param amount to deposit
1068
- * @param marketIndex spot market index to deposit into
1069
- * @param associatedTokenAccount can be the wallet public key if using native sol
1070
- * @param subAccountId subaccountId to deposit
1071
- * @param reduceOnly if true, deposit must not increase account risk
1072
- */
1073
- async deposit(amount, marketIndex, associatedTokenAccount, subAccountId, reduceOnly = false, txParams) {
1074
- const additionalSigners = [];
1066
+ async createDepositTxn(amount, marketIndex, associatedTokenAccount, subAccountId, reduceOnly = false, txParams) {
1075
1067
  const spotMarketAccount = this.getSpotMarketAccount(marketIndex);
1076
1068
  const isSolMarket = spotMarketAccount.mint.equals(spotMarkets_1.WRAPPED_SOL_MINT);
1077
1069
  const signerAuthority = this.wallet.publicKey;
@@ -1090,7 +1082,20 @@ class DriftClient {
1090
1082
  }
1091
1083
  txParams = { ...(txParams !== null && txParams !== void 0 ? txParams : this.txParams), computeUnits: 600000 };
1092
1084
  const tx = await this.buildTransaction(instructions, txParams);
1093
- const { txSig, slot } = await this.sendTransaction(tx, additionalSigners, this.opts);
1085
+ return tx;
1086
+ }
1087
+ /**
1088
+ * Deposit funds into the given spot market
1089
+ *
1090
+ * @param amount to deposit
1091
+ * @param marketIndex spot market index to deposit into
1092
+ * @param associatedTokenAccount can be the wallet public key if using native sol
1093
+ * @param subAccountId subaccountId to deposit
1094
+ * @param reduceOnly if true, deposit must not increase account risk
1095
+ */
1096
+ async deposit(amount, marketIndex, associatedTokenAccount, subAccountId, reduceOnly = false, txParams) {
1097
+ const tx = await this.createDepositTxn(amount, marketIndex, associatedTokenAccount, subAccountId, reduceOnly, txParams);
1098
+ const { txSig, slot } = await this.sendTransaction(tx, [], this.opts);
1094
1099
  this.spotMarketLastSlotCache.set(marketIndex, slot);
1095
1100
  return txSig;
1096
1101
  }
@@ -1165,23 +1170,9 @@ class DriftClient {
1165
1170
  getAssociatedTokenAccountCreationIx(tokenMintAddress, associatedTokenAddress) {
1166
1171
  return (0, spl_token_1.createAssociatedTokenAccountInstruction)(this.wallet.publicKey, associatedTokenAddress, this.wallet.publicKey, tokenMintAddress);
1167
1172
  }
1168
- /**
1169
- * Creates the User account for a user, and deposits some initial collateral
1170
- * @param amount
1171
- * @param userTokenAccount
1172
- * @param marketIndex
1173
- * @param subAccountId
1174
- * @param name
1175
- * @param fromSubAccountId
1176
- * @param referrerInfo
1177
- * @param donateAmount
1178
- * @param txParams
1179
- * @returns
1180
- */
1181
- async initializeUserAccountAndDepositCollateral(amount, userTokenAccount, marketIndex = 0, subAccountId = 0, name, fromSubAccountId, referrerInfo, donateAmount, txParams, customMaxMarginRatio) {
1173
+ async createInitializeUserAccountAndDepositCollateral(amount, userTokenAccount, marketIndex = 0, subAccountId = 0, name, fromSubAccountId, referrerInfo, donateAmount, txParams, customMaxMarginRatio) {
1182
1174
  const ixs = [];
1183
1175
  const [userAccountPublicKey, initializeUserAccountIx] = await this.getInitializeUserInstructions(subAccountId, name, referrerInfo);
1184
- const additionalSigners = [];
1185
1176
  const spotMarket = this.getSpotMarketAccount(marketIndex);
1186
1177
  const isSolMarket = spotMarket.mint.equals(spotMarkets_1.WRAPPED_SOL_MINT);
1187
1178
  const authority = this.wallet.publicKey;
@@ -1226,6 +1217,24 @@ class DriftClient {
1226
1217
  ixs.push((0, spl_token_1.createCloseAccountInstruction)(wsolTokenAccount, authority, authority, []));
1227
1218
  }
1228
1219
  const tx = await this.buildTransaction(ixs, txParams);
1220
+ return [tx, userAccountPublicKey];
1221
+ }
1222
+ /**
1223
+ * Creates the User account for a user, and deposits some initial collateral
1224
+ * @param amount
1225
+ * @param userTokenAccount
1226
+ * @param marketIndex
1227
+ * @param subAccountId
1228
+ * @param name
1229
+ * @param fromSubAccountId
1230
+ * @param referrerInfo
1231
+ * @param donateAmount
1232
+ * @param txParams
1233
+ * @returns
1234
+ */
1235
+ async initializeUserAccountAndDepositCollateral(amount, userTokenAccount, marketIndex = 0, subAccountId = 0, name, fromSubAccountId, referrerInfo, donateAmount, txParams, customMaxMarginRatio) {
1236
+ const [tx, userAccountPublicKey] = await this.createInitializeUserAccountAndDepositCollateral(amount, userTokenAccount, marketIndex, subAccountId, name, fromSubAccountId, referrerInfo, donateAmount, txParams, customMaxMarginRatio);
1237
+ const additionalSigners = [];
1229
1238
  const { txSig, slot } = await this.sendTransaction(tx, additionalSigners, this.opts);
1230
1239
  this.spotMarketLastSlotCache.set(marketIndex, slot);
1231
1240
  await this.addUser(subAccountId);
@@ -3636,9 +3645,7 @@ class DriftClient {
3636
3645
  }
3637
3646
  }
3638
3647
  isVersionedTransaction(tx) {
3639
- const version = tx === null || tx === void 0 ? void 0 : tx.version;
3640
- const isVersionedTx = tx instanceof web3_js_1.VersionedTransaction || version !== undefined;
3641
- return isVersionedTx;
3648
+ return (0, utils_2.isVersionedTransaction)(tx);
3642
3649
  }
3643
3650
  sendTransaction(tx, additionalSigners, opts, preSigned) {
3644
3651
  const isVersionedTx = this.isVersionedTransaction(tx);
@@ -4,7 +4,7 @@ import { OracleInfo } from './oracles/types';
4
4
  import { BulkAccountLoader } from './accounts/bulkAccountLoader';
5
5
  import { DriftEnv } from './config';
6
6
  import { TxSender } from './tx/types';
7
- import { TxHandler } from './tx/txHandler';
7
+ import { TxHandler, TxHandlerConfig } from './tx/txHandler';
8
8
  export type DriftClientConfig = {
9
9
  connection: Connection;
10
10
  wallet: IWallet;
@@ -28,6 +28,7 @@ export type DriftClientConfig = {
28
28
  txVersion?: TransactionVersion;
29
29
  txParams?: TxParams;
30
30
  enableMetricsEvents?: boolean;
31
+ txHandlerConfig?: TxHandlerConfig;
31
32
  };
32
33
  export type DriftClientSubscriptionConfig = {
33
34
  type: 'websocket';
@@ -0,0 +1,8 @@
1
+ import { BlockhashWithExpiryBlockHeight, Commitment, Connection } from '@solana/web3.js';
2
+ import { BlockhashFetcher } from './types';
3
+ export declare class BaseBlockhashFetcher implements BlockhashFetcher {
4
+ private connection;
5
+ private blockhashCommitment;
6
+ constructor(connection: Connection, blockhashCommitment: Commitment);
7
+ getLatestBlockhash(): Promise<BlockhashWithExpiryBlockHeight | undefined>;
8
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseBlockhashFetcher = void 0;
4
+ class BaseBlockhashFetcher {
5
+ constructor(connection, blockhashCommitment) {
6
+ this.connection = connection;
7
+ this.blockhashCommitment = blockhashCommitment;
8
+ }
9
+ async getLatestBlockhash() {
10
+ return this.connection.getLatestBlockhash(this.blockhashCommitment);
11
+ }
12
+ }
13
+ exports.BaseBlockhashFetcher = BaseBlockhashFetcher;
@@ -0,0 +1,28 @@
1
+ import { BlockhashWithExpiryBlockHeight, Commitment, Connection } from '@solana/web3.js';
2
+ import { BlockhashFetcher } from './types';
3
+ /**
4
+ * Fetches the latest blockhash and caches it for a configurable amount of time.
5
+ *
6
+ * - Prevents RPC spam by reusing cached values
7
+ * - Retries on failure with exponential backoff
8
+ * - Prevents concurrent requests for the same blockhash
9
+ */
10
+ export declare class CachedBlockhashFetcher implements BlockhashFetcher {
11
+ private connection;
12
+ private blockhashCommitment;
13
+ private retryCount;
14
+ private retrySleepTimeMs;
15
+ private staleCacheTimeMs;
16
+ private recentBlockhashCache;
17
+ private blockhashFetchingPromise;
18
+ constructor(connection: Connection, blockhashCommitment: Commitment, retryCount: number, retrySleepTimeMs: number, staleCacheTimeMs: number);
19
+ private fetchBlockhashWithRetry;
20
+ private sleep;
21
+ private updateBlockhashCache;
22
+ getLatestBlockhash(): Promise<BlockhashWithExpiryBlockHeight | undefined>;
23
+ private isCacheStale;
24
+ /**
25
+ * Refresh the blockhash cache, await a pending refresh if it exists
26
+ */
27
+ private refreshBlockhash;
28
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CachedBlockhashFetcher = void 0;
4
+ /**
5
+ * Fetches the latest blockhash and caches it for a configurable amount of time.
6
+ *
7
+ * - Prevents RPC spam by reusing cached values
8
+ * - Retries on failure with exponential backoff
9
+ * - Prevents concurrent requests for the same blockhash
10
+ */
11
+ class CachedBlockhashFetcher {
12
+ constructor(connection, blockhashCommitment, retryCount, retrySleepTimeMs, staleCacheTimeMs) {
13
+ this.connection = connection;
14
+ this.blockhashCommitment = blockhashCommitment;
15
+ this.retryCount = retryCount;
16
+ this.retrySleepTimeMs = retrySleepTimeMs;
17
+ this.staleCacheTimeMs = staleCacheTimeMs;
18
+ this.recentBlockhashCache = { value: undefined, lastUpdated: 0 };
19
+ this.blockhashFetchingPromise = null;
20
+ }
21
+ async fetchBlockhashWithRetry() {
22
+ for (let i = 0; i < this.retryCount; i++) {
23
+ try {
24
+ return await this.connection.getLatestBlockhash(this.blockhashCommitment);
25
+ }
26
+ catch (err) {
27
+ if (i === this.retryCount - 1) {
28
+ throw new Error('Failed to fetch blockhash after maximum retries');
29
+ }
30
+ await this.sleep(this.retrySleepTimeMs * 2 ** i);
31
+ }
32
+ }
33
+ throw new Error('Failed to fetch blockhash after maximum retries');
34
+ }
35
+ sleep(ms) {
36
+ return new Promise((resolve) => setTimeout(resolve, ms));
37
+ }
38
+ async updateBlockhashCache() {
39
+ const result = await this.fetchBlockhashWithRetry();
40
+ this.recentBlockhashCache = {
41
+ value: result,
42
+ lastUpdated: Date.now(),
43
+ };
44
+ }
45
+ async getLatestBlockhash() {
46
+ if (this.isCacheStale()) {
47
+ await this.refreshBlockhash();
48
+ }
49
+ return this.recentBlockhashCache.value;
50
+ }
51
+ isCacheStale() {
52
+ const lastUpdateTime = this.recentBlockhashCache.lastUpdated;
53
+ return (!lastUpdateTime || Date.now() > lastUpdateTime + this.staleCacheTimeMs);
54
+ }
55
+ /**
56
+ * Refresh the blockhash cache, await a pending refresh if it exists
57
+ */
58
+ async refreshBlockhash() {
59
+ if (!this.blockhashFetchingPromise) {
60
+ this.blockhashFetchingPromise = this.updateBlockhashCache();
61
+ try {
62
+ await this.blockhashFetchingPromise;
63
+ }
64
+ finally {
65
+ this.blockhashFetchingPromise = null;
66
+ }
67
+ }
68
+ else {
69
+ await this.blockhashFetchingPromise;
70
+ }
71
+ }
72
+ }
73
+ exports.CachedBlockhashFetcher = CachedBlockhashFetcher;
@@ -0,0 +1,4 @@
1
+ import { BlockhashWithExpiryBlockHeight } from '@solana/web3.js';
2
+ export interface BlockhashFetcher {
3
+ getLatestBlockhash(): Promise<BlockhashWithExpiryBlockHeight | undefined>;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -13,6 +13,14 @@ export type TxBuildingProps = {
13
13
  recentBlockhash?: BlockhashWithExpiryBlockHeight;
14
14
  wallet?: IWallet;
15
15
  };
16
+ export type TxHandlerConfig = {
17
+ blockhashCachingEnabled?: boolean;
18
+ blockhashCachingConfig?: {
19
+ retryCount: number;
20
+ retrySleepTimeMs: number;
21
+ staleCacheTimeMs: number;
22
+ };
23
+ };
16
24
  /**
17
25
  * This class is responsible for creating and signing transactions.
18
26
  */
@@ -25,6 +33,7 @@ export declare class TxHandler {
25
33
  private preSignedCb?;
26
34
  private onSignedCb?;
27
35
  private blockhashCommitment;
36
+ private blockHashFetcher;
28
37
  constructor(props: {
29
38
  connection: Connection;
30
39
  wallet: IWallet;
@@ -34,6 +43,7 @@ export declare class TxHandler {
34
43
  onSignedCb?: (txSigs: DriftClientMetricsEvents['txSigned']) => void;
35
44
  preSignedCb?: () => void;
36
45
  };
46
+ config?: TxHandlerConfig;
37
47
  });
38
48
  private addHashAndExpiryToLookup;
39
49
  private getProps;
@@ -8,6 +8,9 @@ const web3_js_1 = require("@solana/web3.js");
8
8
  const txParamProcessor_1 = require("./txParamProcessor");
9
9
  const bs58_1 = __importDefault(require("bs58"));
10
10
  const computeUnits_1 = require("../util/computeUnits");
11
+ const cachedBlockhashFetcher_1 = require("./blockhashFetcher/cachedBlockhashFetcher");
12
+ const baseBlockhashFetcher_1 = require("./blockhashFetcher/baseBlockhashFetcher");
13
+ const utils_1 = require("./utils");
11
14
  /**
12
15
  * Explanation for SIGNATURE_BLOCK_AND_EXPIRY:
13
16
  *
@@ -15,12 +18,15 @@ const computeUnits_1 = require("../util/computeUnits");
15
18
  */
16
19
  const DEV_TRY_FORCE_TX_TIMEOUTS = process.env.DEV_TRY_FORCE_TX_TIMEOUTS === 'true' || false;
17
20
  exports.COMPUTE_UNITS_DEFAULT = 200000;
21
+ const BLOCKHASH_FETCH_RETRY_COUNT = 3;
22
+ const BLOCKHASH_FETCH_RETRY_SLEEP = 200;
23
+ const RECENT_BLOCKHASH_STALE_TIME_MS = 2000; // Reuse blockhashes within this timeframe during bursts of tx contruction
18
24
  /**
19
25
  * This class is responsible for creating and signing transactions.
20
26
  */
21
27
  class TxHandler {
22
28
  constructor(props) {
23
- var _a, _b, _c, _d;
29
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
24
30
  this.blockHashToLastValidBlockHeightLookup = {};
25
31
  this.returnBlockHeightsWithSignedTxCallbackData = false;
26
32
  this.blockhashCommitment = 'finalized';
@@ -28,11 +34,14 @@ class TxHandler {
28
34
  this.connection = props.connection;
29
35
  this.wallet = props.wallet;
30
36
  this.confirmationOptions = props.confirmationOptions;
37
+ this.blockHashFetcher = ((_a = props === null || props === void 0 ? void 0 : props.config) === null || _a === void 0 ? void 0 : _a.blockhashCachingEnabled)
38
+ ? new cachedBlockhashFetcher_1.CachedBlockhashFetcher(this.connection, this.blockhashCommitment, (_d = (_c = (_b = props === null || props === void 0 ? void 0 : props.config) === null || _b === void 0 ? void 0 : _b.blockhashCachingConfig) === null || _c === void 0 ? void 0 : _c.retryCount) !== null && _d !== void 0 ? _d : BLOCKHASH_FETCH_RETRY_COUNT, (_g = (_f = (_e = props === null || props === void 0 ? void 0 : props.config) === null || _e === void 0 ? void 0 : _e.blockhashCachingConfig) === null || _f === void 0 ? void 0 : _f.retrySleepTimeMs) !== null && _g !== void 0 ? _g : BLOCKHASH_FETCH_RETRY_SLEEP, (_k = (_j = (_h = props === null || props === void 0 ? void 0 : props.config) === null || _h === void 0 ? void 0 : _h.blockhashCachingConfig) === null || _j === void 0 ? void 0 : _j.staleCacheTimeMs) !== null && _k !== void 0 ? _k : RECENT_BLOCKHASH_STALE_TIME_MS)
39
+ : new baseBlockhashFetcher_1.BaseBlockhashFetcher(this.connection, this.blockhashCommitment);
31
40
  // #Optionals
32
41
  this.returnBlockHeightsWithSignedTxCallbackData =
33
- (_b = (_a = props.opts) === null || _a === void 0 ? void 0 : _a.returnBlockHeightsWithSignedTxCallbackData) !== null && _b !== void 0 ? _b : false;
34
- this.onSignedCb = (_c = props.opts) === null || _c === void 0 ? void 0 : _c.onSignedCb;
35
- this.preSignedCb = (_d = props.opts) === null || _d === void 0 ? void 0 : _d.preSignedCb;
42
+ (_m = (_l = props.opts) === null || _l === void 0 ? void 0 : _l.returnBlockHeightsWithSignedTxCallbackData) !== null && _m !== void 0 ? _m : false;
43
+ this.onSignedCb = (_o = props.opts) === null || _o === void 0 ? void 0 : _o.onSignedCb;
44
+ this.preSignedCb = (_p = props.opts) === null || _p === void 0 ? void 0 : _p.preSignedCb;
36
45
  }
37
46
  addHashAndExpiryToLookup(hashAndExpiry) {
38
47
  if (!this.returnBlockHeightsWithSignedTxCallbackData)
@@ -50,8 +59,8 @@ class TxHandler {
50
59
  *
51
60
  * @returns
52
61
  */
53
- getLatestBlockhashForTransaction() {
54
- return this.connection.getLatestBlockhash(this.blockhashCommitment);
62
+ async getLatestBlockhashForTransaction() {
63
+ return this.blockHashFetcher.getLatestBlockhash();
55
64
  }
56
65
  /**
57
66
  * Applies recent blockhash and signs a given transaction
@@ -80,7 +89,7 @@ class TxHandler {
80
89
  return signedTx;
81
90
  }
82
91
  isVersionedTransaction(tx) {
83
- return (tx === null || tx === void 0 ? void 0 : tx.message) && true;
92
+ return (0, utils_1.isVersionedTransaction)(tx);
84
93
  }
85
94
  isLegacyTransaction(tx) {
86
95
  return !this.isVersionedTransaction(tx);
@@ -0,0 +1,2 @@
1
+ import { Transaction, VersionedTransaction } from '@solana/web3.js';
2
+ export declare const isVersionedTransaction: (tx: Transaction | VersionedTransaction) => boolean;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isVersionedTransaction = void 0;
4
+ const web3_js_1 = require("@solana/web3.js");
5
+ const isVersionedTransaction = (tx) => {
6
+ const version = tx === null || tx === void 0 ? void 0 : tx.version;
7
+ const isVersionedTx = tx instanceof web3_js_1.VersionedTransaction || version !== undefined;
8
+ return isVersionedTx;
9
+ };
10
+ exports.isVersionedTransaction = isVersionedTransaction;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drift-labs/sdk",
3
- "version": "2.85.0-beta.10",
3
+ "version": "2.85.0-beta.11",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "author": "crispheaney",
@@ -65,6 +65,7 @@
65
65
  "mocha": "^10.0.0",
66
66
  "object-sizeof": "^2.6.3",
67
67
  "prettier": "3.0.1",
68
+ "sinon": "^18.0.0",
68
69
  "ts-node": "^10.8.0",
69
70
  "typescript": "^4.9.5"
70
71
  },
@@ -129,6 +129,7 @@ import { numberToSafeBN } from './math/utils';
129
129
  import { TransactionParamProcessor } from './tx/txParamProcessor';
130
130
  import { isOracleValid } from './math/oracles';
131
131
  import { TxHandler } from './tx/txHandler';
132
+ import { isVersionedTransaction } from './tx/utils';
132
133
 
133
134
  type RemainingAccountParams = {
134
135
  userAccounts: UserAccount[];
@@ -227,6 +228,7 @@ export class DriftClient {
227
228
  onSignedCb: this.handleSignedTransaction.bind(this),
228
229
  preSignedCb: this.handlePreSignedTransaction.bind(this),
229
230
  },
231
+ config: config.txHandlerConfig,
230
232
  });
231
233
 
232
234
  if (config.includeDelegates && config.subAccountIds) {
@@ -1800,25 +1802,14 @@ export class DriftClient {
1800
1802
  });
1801
1803
  }
1802
1804
 
1803
- /**
1804
- * Deposit funds into the given spot market
1805
- *
1806
- * @param amount to deposit
1807
- * @param marketIndex spot market index to deposit into
1808
- * @param associatedTokenAccount can be the wallet public key if using native sol
1809
- * @param subAccountId subaccountId to deposit
1810
- * @param reduceOnly if true, deposit must not increase account risk
1811
- */
1812
- public async deposit(
1805
+ public async createDepositTxn(
1813
1806
  amount: BN,
1814
1807
  marketIndex: number,
1815
1808
  associatedTokenAccount: PublicKey,
1816
1809
  subAccountId?: number,
1817
1810
  reduceOnly = false,
1818
1811
  txParams?: TxParams
1819
- ): Promise<TransactionSignature> {
1820
- const additionalSigners: Array<Signer> = [];
1821
-
1812
+ ): Promise<ReturnType<typeof this.buildTransaction>> {
1822
1813
  const spotMarketAccount = this.getSpotMarketAccount(marketIndex);
1823
1814
 
1824
1815
  const isSolMarket = spotMarketAccount.mint.equals(WRAPPED_SOL_MINT);
@@ -1868,11 +1859,36 @@ export class DriftClient {
1868
1859
 
1869
1860
  const tx = await this.buildTransaction(instructions, txParams);
1870
1861
 
1871
- const { txSig, slot } = await this.sendTransaction(
1872
- tx,
1873
- additionalSigners,
1874
- this.opts
1862
+ return tx;
1863
+ }
1864
+
1865
+ /**
1866
+ * Deposit funds into the given spot market
1867
+ *
1868
+ * @param amount to deposit
1869
+ * @param marketIndex spot market index to deposit into
1870
+ * @param associatedTokenAccount can be the wallet public key if using native sol
1871
+ * @param subAccountId subaccountId to deposit
1872
+ * @param reduceOnly if true, deposit must not increase account risk
1873
+ */
1874
+ public async deposit(
1875
+ amount: BN,
1876
+ marketIndex: number,
1877
+ associatedTokenAccount: PublicKey,
1878
+ subAccountId?: number,
1879
+ reduceOnly = false,
1880
+ txParams?: TxParams
1881
+ ): Promise<TransactionSignature> {
1882
+ const tx = await this.createDepositTxn(
1883
+ amount,
1884
+ marketIndex,
1885
+ associatedTokenAccount,
1886
+ subAccountId,
1887
+ reduceOnly,
1888
+ txParams
1875
1889
  );
1890
+
1891
+ const { txSig, slot } = await this.sendTransaction(tx, [], this.opts);
1876
1892
  this.spotMarketLastSlotCache.set(marketIndex, slot);
1877
1893
  return txSig;
1878
1894
  }
@@ -2005,20 +2021,7 @@ export class DriftClient {
2005
2021
  );
2006
2022
  }
2007
2023
 
2008
- /**
2009
- * Creates the User account for a user, and deposits some initial collateral
2010
- * @param amount
2011
- * @param userTokenAccount
2012
- * @param marketIndex
2013
- * @param subAccountId
2014
- * @param name
2015
- * @param fromSubAccountId
2016
- * @param referrerInfo
2017
- * @param donateAmount
2018
- * @param txParams
2019
- * @returns
2020
- */
2021
- public async initializeUserAccountAndDepositCollateral(
2024
+ public async createInitializeUserAccountAndDepositCollateral(
2022
2025
  amount: BN,
2023
2026
  userTokenAccount: PublicKey,
2024
2027
  marketIndex = 0,
@@ -2029,7 +2032,7 @@ export class DriftClient {
2029
2032
  donateAmount?: BN,
2030
2033
  txParams?: TxParams,
2031
2034
  customMaxMarginRatio?: number
2032
- ): Promise<[TransactionSignature, PublicKey]> {
2035
+ ): Promise<[Transaction | VersionedTransaction, PublicKey]> {
2033
2036
  const ixs = [];
2034
2037
 
2035
2038
  const [userAccountPublicKey, initializeUserAccountIx] =
@@ -2039,8 +2042,6 @@ export class DriftClient {
2039
2042
  referrerInfo
2040
2043
  );
2041
2044
 
2042
- const additionalSigners: Array<Signer> = [];
2043
-
2044
2045
  const spotMarket = this.getSpotMarketAccount(marketIndex);
2045
2046
 
2046
2047
  const isSolMarket = spotMarket.mint.equals(WRAPPED_SOL_MINT);
@@ -2134,6 +2135,49 @@ export class DriftClient {
2134
2135
 
2135
2136
  const tx = await this.buildTransaction(ixs, txParams);
2136
2137
 
2138
+ return [tx, userAccountPublicKey];
2139
+ }
2140
+
2141
+ /**
2142
+ * Creates the User account for a user, and deposits some initial collateral
2143
+ * @param amount
2144
+ * @param userTokenAccount
2145
+ * @param marketIndex
2146
+ * @param subAccountId
2147
+ * @param name
2148
+ * @param fromSubAccountId
2149
+ * @param referrerInfo
2150
+ * @param donateAmount
2151
+ * @param txParams
2152
+ * @returns
2153
+ */
2154
+ public async initializeUserAccountAndDepositCollateral(
2155
+ amount: BN,
2156
+ userTokenAccount: PublicKey,
2157
+ marketIndex = 0,
2158
+ subAccountId = 0,
2159
+ name?: string,
2160
+ fromSubAccountId?: number,
2161
+ referrerInfo?: ReferrerInfo,
2162
+ donateAmount?: BN,
2163
+ txParams?: TxParams,
2164
+ customMaxMarginRatio?: number
2165
+ ): Promise<[TransactionSignature, PublicKey]> {
2166
+ const [tx, userAccountPublicKey] =
2167
+ await this.createInitializeUserAccountAndDepositCollateral(
2168
+ amount,
2169
+ userTokenAccount,
2170
+ marketIndex,
2171
+ subAccountId,
2172
+ name,
2173
+ fromSubAccountId,
2174
+ referrerInfo,
2175
+ donateAmount,
2176
+ txParams,
2177
+ customMaxMarginRatio
2178
+ );
2179
+ const additionalSigners: Array<Signer> = [];
2180
+
2137
2181
  const { txSig, slot } = await this.sendTransaction(
2138
2182
  tx,
2139
2183
  additionalSigners,
@@ -6863,11 +6907,7 @@ export class DriftClient {
6863
6907
  private isVersionedTransaction(
6864
6908
  tx: Transaction | VersionedTransaction
6865
6909
  ): boolean {
6866
- const version = (tx as VersionedTransaction)?.version;
6867
- const isVersionedTx =
6868
- tx instanceof VersionedTransaction || version !== undefined;
6869
-
6870
- return isVersionedTx;
6910
+ return isVersionedTransaction(tx);
6871
6911
  }
6872
6912
 
6873
6913
  sendTransaction(
@@ -10,7 +10,7 @@ import { OracleInfo } from './oracles/types';
10
10
  import { BulkAccountLoader } from './accounts/bulkAccountLoader';
11
11
  import { DriftEnv } from './config';
12
12
  import { TxSender } from './tx/types';
13
- import { TxHandler } from './tx/txHandler';
13
+ import { TxHandler, TxHandlerConfig } from './tx/txHandler';
14
14
 
15
15
  export type DriftClientConfig = {
16
16
  connection: Connection;
@@ -35,6 +35,7 @@ export type DriftClientConfig = {
35
35
  txVersion?: TransactionVersion; // which tx version to use
36
36
  txParams?: TxParams; // default tx params to use
37
37
  enableMetricsEvents?: boolean;
38
+ txHandlerConfig?: TxHandlerConfig;
38
39
  };
39
40
 
40
41
  export type DriftClientSubscriptionConfig =
@@ -0,0 +1,19 @@
1
+ import {
2
+ BlockhashWithExpiryBlockHeight,
3
+ Commitment,
4
+ Connection,
5
+ } from '@solana/web3.js';
6
+ import { BlockhashFetcher } from './types';
7
+
8
+ export class BaseBlockhashFetcher implements BlockhashFetcher {
9
+ constructor(
10
+ private connection: Connection,
11
+ private blockhashCommitment: Commitment
12
+ ) {}
13
+
14
+ public async getLatestBlockhash(): Promise<
15
+ BlockhashWithExpiryBlockHeight | undefined
16
+ > {
17
+ return this.connection.getLatestBlockhash(this.blockhashCommitment);
18
+ }
19
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ BlockhashWithExpiryBlockHeight,
3
+ Commitment,
4
+ Connection,
5
+ } from '@solana/web3.js';
6
+ import { BlockhashFetcher } from './types';
7
+
8
+ /**
9
+ * Fetches the latest blockhash and caches it for a configurable amount of time.
10
+ *
11
+ * - Prevents RPC spam by reusing cached values
12
+ * - Retries on failure with exponential backoff
13
+ * - Prevents concurrent requests for the same blockhash
14
+ */
15
+ export class CachedBlockhashFetcher implements BlockhashFetcher {
16
+ private recentBlockhashCache: {
17
+ value: BlockhashWithExpiryBlockHeight | undefined;
18
+ lastUpdated: number;
19
+ } = { value: undefined, lastUpdated: 0 };
20
+
21
+ private blockhashFetchingPromise: Promise<void> | null = null;
22
+
23
+ constructor(
24
+ private connection: Connection,
25
+ private blockhashCommitment: Commitment,
26
+ private retryCount: number,
27
+ private retrySleepTimeMs: number,
28
+ private staleCacheTimeMs: number
29
+ ) {}
30
+
31
+ private async fetchBlockhashWithRetry(): Promise<BlockhashWithExpiryBlockHeight> {
32
+ for (let i = 0; i < this.retryCount; i++) {
33
+ try {
34
+ return await this.connection.getLatestBlockhash(
35
+ this.blockhashCommitment
36
+ );
37
+ } catch (err) {
38
+ if (i === this.retryCount - 1) {
39
+ throw new Error('Failed to fetch blockhash after maximum retries');
40
+ }
41
+ await this.sleep(this.retrySleepTimeMs * 2 ** i);
42
+ }
43
+ }
44
+ throw new Error('Failed to fetch blockhash after maximum retries');
45
+ }
46
+
47
+ private sleep(ms: number): Promise<void> {
48
+ return new Promise((resolve) => setTimeout(resolve, ms));
49
+ }
50
+
51
+ private async updateBlockhashCache(): Promise<void> {
52
+ const result = await this.fetchBlockhashWithRetry();
53
+ this.recentBlockhashCache = {
54
+ value: result,
55
+ lastUpdated: Date.now(),
56
+ };
57
+ }
58
+
59
+ public async getLatestBlockhash(): Promise<
60
+ BlockhashWithExpiryBlockHeight | undefined
61
+ > {
62
+ if (this.isCacheStale()) {
63
+ await this.refreshBlockhash();
64
+ }
65
+ return this.recentBlockhashCache.value;
66
+ }
67
+
68
+ private isCacheStale(): boolean {
69
+ const lastUpdateTime = this.recentBlockhashCache.lastUpdated;
70
+ return (
71
+ !lastUpdateTime || Date.now() > lastUpdateTime + this.staleCacheTimeMs
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Refresh the blockhash cache, await a pending refresh if it exists
77
+ */
78
+ private async refreshBlockhash(): Promise<void> {
79
+ if (!this.blockhashFetchingPromise) {
80
+ this.blockhashFetchingPromise = this.updateBlockhashCache();
81
+ try {
82
+ await this.blockhashFetchingPromise;
83
+ } finally {
84
+ this.blockhashFetchingPromise = null;
85
+ }
86
+ } else {
87
+ await this.blockhashFetchingPromise;
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,5 @@
1
+ import { BlockhashWithExpiryBlockHeight } from '@solana/web3.js';
2
+
3
+ export interface BlockhashFetcher {
4
+ getLatestBlockhash(): Promise<BlockhashWithExpiryBlockHeight | undefined>;
5
+ }
@@ -25,6 +25,10 @@ import {
25
25
  TxParams,
26
26
  } from '../types';
27
27
  import { containsComputeUnitIxs } from '../util/computeUnits';
28
+ import { CachedBlockhashFetcher } from './blockhashFetcher/cachedBlockhashFetcher';
29
+ import { BaseBlockhashFetcher } from './blockhashFetcher/baseBlockhashFetcher';
30
+ import { BlockhashFetcher } from './blockhashFetcher/types';
31
+ import { isVersionedTransaction } from './utils';
28
32
 
29
33
  /**
30
34
  * Explanation for SIGNATURE_BLOCK_AND_EXPIRY:
@@ -37,6 +41,10 @@ const DEV_TRY_FORCE_TX_TIMEOUTS =
37
41
 
38
42
  export const COMPUTE_UNITS_DEFAULT = 200_000;
39
43
 
44
+ const BLOCKHASH_FETCH_RETRY_COUNT = 3;
45
+ const BLOCKHASH_FETCH_RETRY_SLEEP = 200;
46
+ const RECENT_BLOCKHASH_STALE_TIME_MS = 2_000; // Reuse blockhashes within this timeframe during bursts of tx contruction
47
+
40
48
  export type TxBuildingProps = {
41
49
  instructions: TransactionInstruction | TransactionInstruction[];
42
50
  txVersion: TransactionVersion;
@@ -50,6 +58,15 @@ export type TxBuildingProps = {
50
58
  wallet?: IWallet;
51
59
  };
52
60
 
61
+ export type TxHandlerConfig = {
62
+ blockhashCachingEnabled?: boolean;
63
+ blockhashCachingConfig?: {
64
+ retryCount: number;
65
+ retrySleepTimeMs: number;
66
+ staleCacheTimeMs: number;
67
+ };
68
+ };
69
+
53
70
  /**
54
71
  * This class is responsible for creating and signing transactions.
55
72
  */
@@ -65,6 +82,7 @@ export class TxHandler {
65
82
  private onSignedCb?: (txSigs: DriftClientMetricsEvents['txSigned']) => void;
66
83
 
67
84
  private blockhashCommitment: Commitment = 'finalized';
85
+ private blockHashFetcher: BlockhashFetcher;
68
86
 
69
87
  constructor(props: {
70
88
  connection: Connection;
@@ -75,11 +93,25 @@ export class TxHandler {
75
93
  onSignedCb?: (txSigs: DriftClientMetricsEvents['txSigned']) => void;
76
94
  preSignedCb?: () => void;
77
95
  };
96
+ config?: TxHandlerConfig;
78
97
  }) {
79
98
  this.connection = props.connection;
80
99
  this.wallet = props.wallet;
81
100
  this.confirmationOptions = props.confirmationOptions;
82
101
 
102
+ this.blockHashFetcher = props?.config?.blockhashCachingEnabled
103
+ ? new CachedBlockhashFetcher(
104
+ this.connection,
105
+ this.blockhashCommitment,
106
+ props?.config?.blockhashCachingConfig?.retryCount ??
107
+ BLOCKHASH_FETCH_RETRY_COUNT,
108
+ props?.config?.blockhashCachingConfig?.retrySleepTimeMs ??
109
+ BLOCKHASH_FETCH_RETRY_SLEEP,
110
+ props?.config?.blockhashCachingConfig?.staleCacheTimeMs ??
111
+ RECENT_BLOCKHASH_STALE_TIME_MS
112
+ )
113
+ : new BaseBlockhashFetcher(this.connection, this.blockhashCommitment);
114
+
83
115
  // #Optionals
84
116
  this.returnBlockHeightsWithSignedTxCallbackData =
85
117
  props.opts?.returnBlockHeightsWithSignedTxCallbackData ?? false;
@@ -113,8 +145,8 @@ export class TxHandler {
113
145
  *
114
146
  * @returns
115
147
  */
116
- public getLatestBlockhashForTransaction() {
117
- return this.connection.getLatestBlockhash(this.blockhashCommitment);
148
+ public async getLatestBlockhashForTransaction() {
149
+ return this.blockHashFetcher.getLatestBlockhash();
118
150
  }
119
151
 
120
152
  /**
@@ -157,8 +189,10 @@ export class TxHandler {
157
189
  return signedTx;
158
190
  }
159
191
 
160
- private isVersionedTransaction(tx: Transaction | VersionedTransaction) {
161
- return (tx as VersionedTransaction)?.message && true;
192
+ private isVersionedTransaction(
193
+ tx: Transaction | VersionedTransaction
194
+ ): boolean {
195
+ return isVersionedTransaction(tx);
162
196
  }
163
197
 
164
198
  private isLegacyTransaction(tx: Transaction | VersionedTransaction) {
@@ -0,0 +1,11 @@
1
+ import { Transaction, VersionedTransaction } from '@solana/web3.js';
2
+
3
+ export const isVersionedTransaction = (
4
+ tx: Transaction | VersionedTransaction
5
+ ): boolean => {
6
+ const version = (tx as VersionedTransaction)?.version;
7
+ const isVersionedTx =
8
+ tx instanceof VersionedTransaction || version !== undefined;
9
+
10
+ return isVersionedTx;
11
+ };
@@ -0,0 +1,96 @@
1
+ import { expect } from 'chai';
2
+ import sinon from 'sinon';
3
+ import {
4
+ Connection,
5
+ Commitment,
6
+ BlockhashWithExpiryBlockHeight,
7
+ } from '@solana/web3.js';
8
+ import { CachedBlockhashFetcher } from '../../src/tx/blockhashFetcher/cachedBlockhashFetcher';
9
+
10
+ describe('CachedBlockhashFetcher', () => {
11
+ let connection: sinon.SinonStubbedInstance<Connection>;
12
+ let cachedBlockhashFetcher: CachedBlockhashFetcher;
13
+ const mockBlockhash: BlockhashWithExpiryBlockHeight = {
14
+ blockhash: 'mockedBlockhash',
15
+ lastValidBlockHeight: 1000,
16
+ };
17
+
18
+ beforeEach(() => {
19
+ connection = sinon.createStubInstance(Connection);
20
+ connection.getLatestBlockhash.resolves(mockBlockhash);
21
+
22
+ cachedBlockhashFetcher = new CachedBlockhashFetcher(
23
+ connection as unknown as Connection,
24
+ 'confirmed' as Commitment,
25
+ 3,
26
+ 100,
27
+ 1000
28
+ );
29
+ });
30
+
31
+ afterEach(() => {
32
+ sinon.restore();
33
+ });
34
+
35
+ it('should fetch and cache the latest blockhash', async () => {
36
+ const result = await cachedBlockhashFetcher.getLatestBlockhash();
37
+ expect(result).to.deep.equal(mockBlockhash);
38
+ expect(connection.getLatestBlockhash.calledOnce).to.be.true;
39
+ });
40
+
41
+ it('should use cached blockhash if not stale', async () => {
42
+ await cachedBlockhashFetcher.getLatestBlockhash();
43
+ await cachedBlockhashFetcher.getLatestBlockhash();
44
+ expect(connection.getLatestBlockhash.calledOnce).to.be.true;
45
+ });
46
+
47
+ it('should refresh blockhash if cache is stale', async () => {
48
+ const clock = sinon.useFakeTimers();
49
+
50
+ await cachedBlockhashFetcher.getLatestBlockhash();
51
+
52
+ // Advance time to make cache stale
53
+ clock.tick(1100);
54
+
55
+ await cachedBlockhashFetcher.getLatestBlockhash();
56
+ expect(connection.getLatestBlockhash.calledTwice).to.be.true;
57
+
58
+ clock.restore();
59
+ });
60
+
61
+ it('should retry on failure', async () => {
62
+ connection.getLatestBlockhash
63
+ .onFirstCall()
64
+ .rejects(new Error('Network error'))
65
+ .onSecondCall()
66
+ .rejects(new Error('Network error'))
67
+ .onThirdCall()
68
+ .resolves(mockBlockhash);
69
+
70
+ const result = await cachedBlockhashFetcher.getLatestBlockhash();
71
+ expect(result).to.deep.equal(mockBlockhash);
72
+ expect(connection.getLatestBlockhash.calledThrice).to.be.true;
73
+ });
74
+
75
+ it('should throw error after maximum retries', async () => {
76
+ connection.getLatestBlockhash.rejects(new Error('Network error'));
77
+
78
+ try {
79
+ await cachedBlockhashFetcher.getLatestBlockhash();
80
+ expect.fail('Should have thrown an error');
81
+ } catch (error) {
82
+ expect(error.message).to.equal(
83
+ 'Failed to fetch blockhash after maximum retries'
84
+ );
85
+ }
86
+ expect(connection.getLatestBlockhash.calledThrice).to.be.true;
87
+ });
88
+
89
+ it('should prevent concurrent requests for the same blockhash', async () => {
90
+ const promise1 = cachedBlockhashFetcher.getLatestBlockhash();
91
+ const promise2 = cachedBlockhashFetcher.getLatestBlockhash();
92
+
93
+ await Promise.all([promise1, promise2]);
94
+ expect(connection.getLatestBlockhash.calledOnce).to.be.true;
95
+ });
96
+ });