@drift-labs/sdk 2.85.0-beta.9 → 2.86.0-beta.1

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/bun.lockb +0 -0
  3. package/lib/addresses/pda.d.ts +1 -0
  4. package/lib/addresses/pda.js +8 -1
  5. package/lib/adminClient.d.ts +2 -0
  6. package/lib/adminClient.js +22 -0
  7. package/lib/config.d.ts +3 -0
  8. package/lib/config.js +4 -1
  9. package/lib/constants/perpMarkets.d.ts +1 -0
  10. package/lib/constants/perpMarkets.js +57 -0
  11. package/lib/constants/spotMarkets.d.ts +1 -0
  12. package/lib/constants/spotMarkets.js +20 -0
  13. package/lib/driftClient.d.ts +26 -1
  14. package/lib/driftClient.js +157 -31
  15. package/lib/driftClientConfig.d.ts +2 -1
  16. package/lib/idl/drift.json +161 -2
  17. package/lib/math/oracles.d.ts +2 -0
  18. package/lib/math/oracles.js +14 -1
  19. package/lib/oracles/pythPullClient.js +1 -1
  20. package/lib/tx/baseTxSender.js +1 -0
  21. package/lib/tx/blockhashFetcher/baseBlockhashFetcher.d.ts +8 -0
  22. package/lib/tx/blockhashFetcher/baseBlockhashFetcher.js +13 -0
  23. package/lib/tx/blockhashFetcher/cachedBlockhashFetcher.d.ts +28 -0
  24. package/lib/tx/blockhashFetcher/cachedBlockhashFetcher.js +73 -0
  25. package/lib/tx/blockhashFetcher/types.d.ts +4 -0
  26. package/lib/tx/blockhashFetcher/types.js +2 -0
  27. package/lib/tx/txHandler.d.ts +10 -0
  28. package/lib/tx/txHandler.js +16 -7
  29. package/lib/tx/utils.d.ts +2 -0
  30. package/lib/tx/utils.js +10 -0
  31. package/lib/util/pythPullOracleUtils.d.ts +2 -0
  32. package/lib/util/pythPullOracleUtils.js +15 -0
  33. package/package.json +6 -1
  34. package/src/addresses/pda.ts +13 -0
  35. package/src/adminClient.ts +39 -0
  36. package/src/config.ts +6 -0
  37. package/src/constants/perpMarkets.ts +115 -0
  38. package/src/constants/spotMarkets.ts +41 -0
  39. package/src/driftClient.ts +347 -41
  40. package/src/driftClientConfig.ts +2 -1
  41. package/src/idl/drift.json +161 -2
  42. package/src/math/oracles.ts +17 -0
  43. package/src/oracles/pythPullClient.ts +2 -3
  44. package/src/tx/baseTxSender.ts +1 -0
  45. package/src/tx/blockhashFetcher/baseBlockhashFetcher.ts +19 -0
  46. package/src/tx/blockhashFetcher/cachedBlockhashFetcher.ts +90 -0
  47. package/src/tx/blockhashFetcher/types.ts +5 -0
  48. package/src/tx/txHandler.ts +38 -4
  49. package/src/tx/utils.ts +11 -0
  50. package/src/util/pythPullOracleUtils.ts +11 -0
  51. package/tests/tx/cachedBlockhashFetcher.test.ts +96 -0
@@ -9,11 +9,10 @@ import {
9
9
  } from '../constants/numericConstants';
10
10
  import {
11
11
  PythSolanaReceiverProgram,
12
- DEFAULT_RECEIVER_PROGRAM_ID,
13
12
  pythSolanaReceiverIdl,
14
13
  } from '@pythnetwork/pyth-solana-receiver';
15
14
  import { PriceUpdateAccount } from '@pythnetwork/pyth-solana-receiver/lib/PythSolanaReceiver';
16
- import { Wallet } from '..';
15
+ import { DRIFT_ORACLE_RECEIVER_ID, Wallet } from '..';
17
16
 
18
17
  export class PythPullClient implements OracleClient {
19
18
  private connection: Connection;
@@ -40,7 +39,7 @@ export class PythPullClient implements OracleClient {
40
39
  );
41
40
  this.receiver = new Program<PythSolanaReceiverProgram>(
42
41
  pythSolanaReceiverIdl as PythSolanaReceiverProgram,
43
- DEFAULT_RECEIVER_PROGRAM_ID,
42
+ DRIFT_ORACLE_RECEIVER_ID,
44
43
  provider
45
44
  );
46
45
  this.decodeFunc =
@@ -394,6 +394,7 @@ export abstract class BaseTxSender implements TxSender {
394
394
 
395
395
  const friendlyMessage = lastLog?.match(/(failed:) (.+)/)?.[2];
396
396
 
397
+ // @ts-ignore
397
398
  throw new SendTransactionError({
398
399
  action: 'send',
399
400
  signature: txSig,
@@ -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,11 @@
1
+ export function trimFeedId(feedId: string): string {
2
+ if (feedId.startsWith('0x')) {
3
+ return feedId.slice(2);
4
+ }
5
+ return feedId;
6
+ }
7
+
8
+ export function getFeedIdUint8Array(feedId: string): Uint8Array {
9
+ const trimmedFeedId = trimFeedId(feedId);
10
+ return Uint8Array.from(Buffer.from(trimmedFeedId, 'hex'));
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
+ });