@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.
- package/VERSION +1 -1
- package/bun.lockb +0 -0
- package/lib/addresses/pda.d.ts +1 -0
- package/lib/addresses/pda.js +8 -1
- package/lib/adminClient.d.ts +2 -0
- package/lib/adminClient.js +22 -0
- package/lib/config.d.ts +3 -0
- package/lib/config.js +4 -1
- package/lib/constants/perpMarkets.d.ts +1 -0
- package/lib/constants/perpMarkets.js +57 -0
- package/lib/constants/spotMarkets.d.ts +1 -0
- package/lib/constants/spotMarkets.js +20 -0
- package/lib/driftClient.d.ts +26 -1
- package/lib/driftClient.js +157 -31
- package/lib/driftClientConfig.d.ts +2 -1
- package/lib/idl/drift.json +161 -2
- package/lib/math/oracles.d.ts +2 -0
- package/lib/math/oracles.js +14 -1
- package/lib/oracles/pythPullClient.js +1 -1
- package/lib/tx/baseTxSender.js +1 -0
- package/lib/tx/blockhashFetcher/baseBlockhashFetcher.d.ts +8 -0
- package/lib/tx/blockhashFetcher/baseBlockhashFetcher.js +13 -0
- package/lib/tx/blockhashFetcher/cachedBlockhashFetcher.d.ts +28 -0
- package/lib/tx/blockhashFetcher/cachedBlockhashFetcher.js +73 -0
- package/lib/tx/blockhashFetcher/types.d.ts +4 -0
- package/lib/tx/blockhashFetcher/types.js +2 -0
- package/lib/tx/txHandler.d.ts +10 -0
- package/lib/tx/txHandler.js +16 -7
- package/lib/tx/utils.d.ts +2 -0
- package/lib/tx/utils.js +10 -0
- package/lib/util/pythPullOracleUtils.d.ts +2 -0
- package/lib/util/pythPullOracleUtils.js +15 -0
- package/package.json +6 -1
- package/src/addresses/pda.ts +13 -0
- package/src/adminClient.ts +39 -0
- package/src/config.ts +6 -0
- package/src/constants/perpMarkets.ts +115 -0
- package/src/constants/spotMarkets.ts +41 -0
- package/src/driftClient.ts +347 -41
- package/src/driftClientConfig.ts +2 -1
- package/src/idl/drift.json +161 -2
- package/src/math/oracles.ts +17 -0
- package/src/oracles/pythPullClient.ts +2 -3
- package/src/tx/baseTxSender.ts +1 -0
- package/src/tx/blockhashFetcher/baseBlockhashFetcher.ts +19 -0
- package/src/tx/blockhashFetcher/cachedBlockhashFetcher.ts +90 -0
- package/src/tx/blockhashFetcher/types.ts +5 -0
- package/src/tx/txHandler.ts +38 -4
- package/src/tx/utils.ts +11 -0
- package/src/util/pythPullOracleUtils.ts +11 -0
- 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
|
-
|
|
42
|
+
DRIFT_ORACLE_RECEIVER_ID,
|
|
44
43
|
provider
|
|
45
44
|
);
|
|
46
45
|
this.decodeFunc =
|
package/src/tx/baseTxSender.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tx/txHandler.ts
CHANGED
|
@@ -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.
|
|
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(
|
|
161
|
-
|
|
192
|
+
private isVersionedTransaction(
|
|
193
|
+
tx: Transaction | VersionedTransaction
|
|
194
|
+
): boolean {
|
|
195
|
+
return isVersionedTransaction(tx);
|
|
162
196
|
}
|
|
163
197
|
|
|
164
198
|
private isLegacyTransaction(tx: Transaction | VersionedTransaction) {
|
package/src/tx/utils.ts
ADDED
|
@@ -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
|
+
});
|