@drift-labs/sdk 2.142.0-beta.13 → 2.142.0-beta.14

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.
@@ -1,4 +1,5 @@
1
- import { WebSocketDriftClientAccountSubscriber } from './webSocketDriftClientAccountSubscriber';
1
+ import StrictEventEmitter from 'strict-event-emitter-types';
2
+ import { EventEmitter } from 'events';
2
3
  import { OracleInfo, OraclePriceData } from '../oracles/types';
3
4
  import { Program } from '@coral-xyz/anchor';
4
5
  import { PublicKey } from '@solana/web3.js';
@@ -6,11 +7,17 @@ import { findAllMarketAndOracles } from '../config';
6
7
  import {
7
8
  getDriftStateAccountPublicKey,
8
9
  getPerpMarketPublicKey,
10
+ getPerpMarketPublicKeySync,
9
11
  getSpotMarketPublicKey,
12
+ getSpotMarketPublicKeySync,
10
13
  } from '../addresses/pda';
11
14
  import {
15
+ AccountSubscriber,
12
16
  DataAndSlot,
13
17
  DelistedMarketSetting,
18
+ DriftClientAccountEvents,
19
+ DriftClientAccountSubscriber,
20
+ NotSubscribedError,
14
21
  GrpcConfigs,
15
22
  ResubOpts,
16
23
  } from './types';
@@ -21,14 +28,44 @@ import {
21
28
  getOracleId,
22
29
  getPublicKeyAndSourceFromOracleId,
23
30
  } from '../oracles/oracleId';
31
+ import { OracleClientCache } from '../oracles/oracleClientCache';
32
+ import { findDelistedPerpMarketsAndOracles } from './utils';
24
33
 
25
- export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAccountSubscriber {
34
+ export class grpcDriftClientAccountSubscriberV2
35
+ implements DriftClientAccountSubscriber
36
+ {
26
37
  private grpcConfigs: GrpcConfigs;
27
38
  private perpMarketsSubscriber?: grpcMultiAccountSubscriber<PerpMarketAccount>;
28
39
  private spotMarketsSubscriber?: grpcMultiAccountSubscriber<SpotMarketAccount>;
29
40
  private oracleMultiSubscriber?: grpcMultiAccountSubscriber<OraclePriceData>;
30
41
  private perpMarketIndexToAccountPubkeyMap = new Map<number, string>();
31
42
  private spotMarketIndexToAccountPubkeyMap = new Map<number, string>();
43
+ private delistedMarketSetting: DelistedMarketSetting;
44
+
45
+ public eventEmitter: StrictEventEmitter<
46
+ EventEmitter,
47
+ DriftClientAccountEvents
48
+ >;
49
+ public isSubscribed: boolean;
50
+ public isSubscribing: boolean;
51
+ public program: Program;
52
+ public perpMarketIndexes: number[];
53
+ public spotMarketIndexes: number[];
54
+ public shouldFindAllMarketsAndOracles: boolean;
55
+ public oracleInfos: OracleInfo[];
56
+ public initialPerpMarketAccountData: Map<number, PerpMarketAccount>;
57
+ public initialSpotMarketAccountData: Map<number, SpotMarketAccount>;
58
+ public initialOraclePriceData: Map<string, OraclePriceData>;
59
+ public perpOracleMap = new Map<number, PublicKey>();
60
+ public perpOracleStringMap = new Map<number, string>();
61
+ public spotOracleMap = new Map<number, PublicKey>();
62
+ public spotOracleStringMap = new Map<number, string>();
63
+ public stateAccountSubscriber?: AccountSubscriber<StateAccount>;
64
+ oracleClientCache = new OracleClientCache();
65
+ private resubOpts?: ResubOpts;
66
+
67
+ private subscriptionPromise: Promise<boolean>;
68
+ protected subscriptionPromiseResolver: (val: boolean) => void;
32
69
 
33
70
  constructor(
34
71
  grpcConfigs: GrpcConfigs,
@@ -40,16 +77,156 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
40
77
  delistedMarketSetting: DelistedMarketSetting,
41
78
  resubOpts?: ResubOpts
42
79
  ) {
43
- super(
44
- program,
45
- perpMarketIndexes,
46
- spotMarketIndexes,
47
- oracleInfos,
48
- shouldFindAllMarketsAndOracles,
49
- delistedMarketSetting,
50
- resubOpts
51
- );
80
+ this.eventEmitter = new EventEmitter();
81
+ this.isSubscribed = false;
82
+ this.isSubscribing = false;
83
+ this.program = program;
84
+ this.perpMarketIndexes = perpMarketIndexes;
85
+ this.spotMarketIndexes = spotMarketIndexes;
86
+ this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles;
87
+ this.oracleInfos = oracleInfos;
88
+ this.initialPerpMarketAccountData = new Map();
89
+ this.initialSpotMarketAccountData = new Map();
90
+ this.initialOraclePriceData = new Map();
91
+ this.perpOracleMap = new Map();
92
+ this.perpOracleStringMap = new Map();
93
+ this.spotOracleMap = new Map();
94
+ this.spotOracleStringMap = new Map();
52
95
  this.grpcConfigs = grpcConfigs;
96
+ this.resubOpts = resubOpts;
97
+ this.delistedMarketSetting = delistedMarketSetting;
98
+ }
99
+
100
+ chunks = <T>(array: readonly T[], size: number): T[][] => {
101
+ return new Array(Math.ceil(array.length / size))
102
+ .fill(null)
103
+ .map((_, index) => index * size)
104
+ .map((begin) => array.slice(begin, begin + size));
105
+ };
106
+
107
+ async setInitialData(): Promise<void> {
108
+ const connection = this.program.provider.connection;
109
+
110
+ if (
111
+ !this.initialPerpMarketAccountData ||
112
+ this.initialPerpMarketAccountData.size === 0
113
+ ) {
114
+ const perpMarketPublicKeys = this.perpMarketIndexes.map((marketIndex) =>
115
+ getPerpMarketPublicKeySync(this.program.programId, marketIndex)
116
+ );
117
+ const perpMarketPublicKeysChunks = this.chunks(perpMarketPublicKeys, 75);
118
+ const perpMarketAccountInfos = (
119
+ await Promise.all(
120
+ perpMarketPublicKeysChunks.map((perpMarketPublicKeysChunk) =>
121
+ connection.getMultipleAccountsInfo(perpMarketPublicKeysChunk)
122
+ )
123
+ )
124
+ ).flat();
125
+ this.initialPerpMarketAccountData = new Map(
126
+ perpMarketAccountInfos
127
+ .filter((accountInfo) => !!accountInfo)
128
+ .map((accountInfo) => {
129
+ const perpMarket = this.program.coder.accounts.decode(
130
+ 'PerpMarket',
131
+ accountInfo.data
132
+ );
133
+ return [perpMarket.marketIndex, perpMarket];
134
+ })
135
+ );
136
+ }
137
+
138
+ if (
139
+ !this.initialSpotMarketAccountData ||
140
+ this.initialSpotMarketAccountData.size === 0
141
+ ) {
142
+ const spotMarketPublicKeys = this.spotMarketIndexes.map((marketIndex) =>
143
+ getSpotMarketPublicKeySync(this.program.programId, marketIndex)
144
+ );
145
+ const spotMarketPublicKeysChunks = this.chunks(spotMarketPublicKeys, 75);
146
+ const spotMarketAccountInfos = (
147
+ await Promise.all(
148
+ spotMarketPublicKeysChunks.map((spotMarketPublicKeysChunk) =>
149
+ connection.getMultipleAccountsInfo(spotMarketPublicKeysChunk)
150
+ )
151
+ )
152
+ ).flat();
153
+ this.initialSpotMarketAccountData = new Map(
154
+ spotMarketAccountInfos
155
+ .filter((accountInfo) => !!accountInfo)
156
+ .map((accountInfo) => {
157
+ const spotMarket = this.program.coder.accounts.decode(
158
+ 'SpotMarket',
159
+ accountInfo.data
160
+ );
161
+ return [spotMarket.marketIndex, spotMarket];
162
+ })
163
+ );
164
+ }
165
+
166
+ const oracleAccountPubkeyChunks = this.chunks(
167
+ this.oracleInfos.map((oracleInfo) => oracleInfo.publicKey),
168
+ 75
169
+ );
170
+ const oracleAccountInfos = (
171
+ await Promise.all(
172
+ oracleAccountPubkeyChunks.map((oracleAccountPublicKeysChunk) =>
173
+ connection.getMultipleAccountsInfo(oracleAccountPublicKeysChunk)
174
+ )
175
+ )
176
+ ).flat();
177
+ this.initialOraclePriceData = new Map(
178
+ this.oracleInfos.reduce((result, oracleInfo, i) => {
179
+ if (!oracleAccountInfos[i]) {
180
+ return result;
181
+ }
182
+ const oracleClient = this.oracleClientCache.get(
183
+ oracleInfo.source,
184
+ connection,
185
+ this.program
186
+ );
187
+ const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer(
188
+ oracleAccountInfos[i].data
189
+ );
190
+ result.push([
191
+ getOracleId(oracleInfo.publicKey, oracleInfo.source),
192
+ oraclePriceData,
193
+ ]);
194
+ return result;
195
+ }, [])
196
+ );
197
+ }
198
+
199
+ async addPerpMarket(_marketIndex: number): Promise<boolean> {
200
+ if (!this.perpMarketIndexes.includes(_marketIndex)) {
201
+ this.perpMarketIndexes = this.perpMarketIndexes.concat(_marketIndex);
202
+ }
203
+ return true;
204
+ }
205
+
206
+ async addSpotMarket(_marketIndex: number): Promise<boolean> {
207
+ return true;
208
+ }
209
+
210
+ async addOracle(oracleInfo: OracleInfo): Promise<boolean> {
211
+ if (oracleInfo.publicKey.equals(PublicKey.default)) {
212
+ return true;
213
+ }
214
+
215
+ const exists = this.oracleInfos.some(
216
+ (o) =>
217
+ o.source === oracleInfo.source &&
218
+ o.publicKey.equals(oracleInfo.publicKey)
219
+ );
220
+ if (!exists) {
221
+ this.oracleInfos = this.oracleInfos.concat(oracleInfo);
222
+ }
223
+
224
+ if (this.oracleMultiSubscriber) {
225
+ await this.unsubscribeFromOracles();
226
+ await this.subscribeToOracles();
227
+ }
228
+
229
+ return true;
53
230
  }
54
231
 
55
232
  public async subscribe(): Promise<boolean> {
@@ -133,7 +310,37 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
133
310
  return true;
134
311
  }
135
312
 
136
- override getMarketAccountAndSlot(
313
+ public async fetch(): Promise<void> {
314
+ await this.stateAccountSubscriber?.fetch();
315
+ await this.perpMarketsSubscriber?.fetch();
316
+ await this.spotMarketsSubscriber?.fetch();
317
+ await this.oracleMultiSubscriber?.fetch();
318
+ }
319
+
320
+ private assertIsSubscribed(): void {
321
+ if (!this.isSubscribed) {
322
+ throw new NotSubscribedError(
323
+ 'You must call `subscribe` before using this function'
324
+ );
325
+ }
326
+ }
327
+
328
+ public getStateAccountAndSlot(): DataAndSlot<StateAccount> {
329
+ this.assertIsSubscribed();
330
+ return this.stateAccountSubscriber.dataAndSlot;
331
+ }
332
+
333
+ public getMarketAccountsAndSlots(): DataAndSlot<PerpMarketAccount>[] {
334
+ const map = this.perpMarketsSubscriber?.getAccountDataMap();
335
+ return Array.from(map?.values() ?? []);
336
+ }
337
+
338
+ public getSpotMarketAccountsAndSlots(): DataAndSlot<SpotMarketAccount>[] {
339
+ const map = this.spotMarketsSubscriber?.getAccountDataMap();
340
+ return Array.from(map?.values() ?? []);
341
+ }
342
+
343
+ getMarketAccountAndSlot(
137
344
  marketIndex: number
138
345
  ): DataAndSlot<PerpMarketAccount> | undefined {
139
346
  return this.perpMarketsSubscriber?.getAccountData(
@@ -141,7 +348,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
141
348
  );
142
349
  }
143
350
 
144
- override getSpotMarketAccountAndSlot(
351
+ getSpotMarketAccountAndSlot(
145
352
  marketIndex: number
146
353
  ): DataAndSlot<SpotMarketAccount> | undefined {
147
354
  return this.spotMarketsSubscriber?.getAccountData(
@@ -149,7 +356,51 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
149
356
  );
150
357
  }
151
358
 
152
- override async setPerpOracleMap() {
359
+ public getOraclePriceDataAndSlot(
360
+ oracleId: string
361
+ ): DataAndSlot<OraclePriceData> | undefined {
362
+ this.assertIsSubscribed();
363
+ const { publicKey } = getPublicKeyAndSourceFromOracleId(oracleId);
364
+ return this.oracleMultiSubscriber?.getAccountData(publicKey.toBase58());
365
+ }
366
+
367
+ public getOraclePriceDataAndSlotForPerpMarket(
368
+ marketIndex: number
369
+ ): DataAndSlot<OraclePriceData> | undefined {
370
+ const perpMarketAccount = this.getMarketAccountAndSlot(marketIndex);
371
+ const oracle = this.perpOracleMap.get(marketIndex);
372
+ const oracleId = this.perpOracleStringMap.get(marketIndex);
373
+ if (!perpMarketAccount || !oracleId) {
374
+ return undefined;
375
+ }
376
+
377
+ if (!perpMarketAccount.data.amm.oracle.equals(oracle)) {
378
+ // If the oracle has changed, we need to update the oracle map in background
379
+ this.setPerpOracleMap();
380
+ }
381
+
382
+ return this.getOraclePriceDataAndSlot(oracleId);
383
+ }
384
+
385
+ public getOraclePriceDataAndSlotForSpotMarket(
386
+ marketIndex: number
387
+ ): DataAndSlot<OraclePriceData> | undefined {
388
+ const spotMarketAccount = this.getSpotMarketAccountAndSlot(marketIndex);
389
+ const oracle = this.spotOracleMap.get(marketIndex);
390
+ const oracleId = this.spotOracleStringMap.get(marketIndex);
391
+ if (!spotMarketAccount || !oracleId) {
392
+ return undefined;
393
+ }
394
+
395
+ if (!spotMarketAccount.data.oracle.equals(oracle)) {
396
+ // If the oracle has changed, we need to update the oracle map in background
397
+ this.setSpotOracleMap();
398
+ }
399
+
400
+ return this.getOraclePriceDataAndSlot(oracleId);
401
+ }
402
+
403
+ async setPerpOracleMap() {
153
404
  const perpMarketsMap = this.perpMarketsSubscriber?.getAccountDataMap();
154
405
  const perpMarkets = Array.from(perpMarketsMap.values());
155
406
  const addOraclePromises = [];
@@ -161,7 +412,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
161
412
  const perpMarketIndex = perpMarketAccount.marketIndex;
162
413
  const oracle = perpMarketAccount.amm.oracle;
163
414
  const oracleId = getOracleId(oracle, perpMarket.data.amm.oracleSource);
164
- if (!this.oracleSubscribers.has(oracleId)) {
415
+ if (!this.oracleMultiSubscriber?.getAccountDataMap().has(oracleId)) {
165
416
  addOraclePromises.push(
166
417
  this.addOracle({
167
418
  publicKey: oracle,
@@ -175,7 +426,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
175
426
  await Promise.all(addOraclePromises);
176
427
  }
177
428
 
178
- override async setSpotOracleMap() {
429
+ async setSpotOracleMap() {
179
430
  const spotMarketsMap = this.spotMarketsSubscriber?.getAccountDataMap();
180
431
  const spotMarkets = Array.from(spotMarketsMap.values());
181
432
  const addOraclePromises = [];
@@ -187,7 +438,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
187
438
  const spotMarketIndex = spotMarketAccount.marketIndex;
188
439
  const oracle = spotMarketAccount.oracle;
189
440
  const oracleId = getOracleId(oracle, spotMarketAccount.oracleSource);
190
- if (!this.oracleSubscribers.has(oracleId)) {
441
+ if (!this.oracleMultiSubscriber?.getAccountDataMap().has(oracleId)) {
191
442
  addOraclePromises.push(
192
443
  this.addOracle({
193
444
  publicKey: oracle,
@@ -201,7 +452,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
201
452
  await Promise.all(addOraclePromises);
202
453
  }
203
454
 
204
- override async subscribeToPerpMarketAccounts(): Promise<boolean> {
455
+ async subscribeToPerpMarketAccounts(): Promise<boolean> {
205
456
  const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> =
206
457
  await Promise.all(
207
458
  this.perpMarketIndexes.map(async (marketIndex) => [
@@ -263,7 +514,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
263
514
  return true;
264
515
  }
265
516
 
266
- override async subscribeToSpotMarketAccounts(): Promise<boolean> {
517
+ async subscribeToSpotMarketAccounts(): Promise<boolean> {
267
518
  const spotMarketIndexToAccountPubkeys: Array<[number, PublicKey]> =
268
519
  await Promise.all(
269
520
  this.spotMarketIndexes.map(async (marketIndex) => [
@@ -325,7 +576,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
325
576
  return true;
326
577
  }
327
578
 
328
- override async subscribeToOracles(): Promise<boolean> {
579
+ async subscribeToOracles(): Promise<boolean> {
329
580
  const pubkeyToSources = new Map<string, Set<OracleInfo['source']>>();
330
581
  for (const info of this.oracleInfos) {
331
582
  if (info.publicKey.equals((PublicKey as any).default)) {
@@ -407,20 +658,56 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco
407
658
  return true;
408
659
  }
409
660
 
661
+ async handleDelistedMarkets(): Promise<void> {
662
+ if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) {
663
+ return;
664
+ }
665
+
666
+ const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles(
667
+ Array.from(
668
+ this.perpMarketsSubscriber?.getAccountDataMap().values() || []
669
+ ),
670
+ Array.from(this.spotMarketsSubscriber?.getAccountDataMap().values() || [])
671
+ );
672
+
673
+ for (const perpMarketIndex of perpMarketIndexes) {
674
+ await this.perpMarketsSubscriber.removeAccounts([
675
+ new PublicKey(
676
+ this.perpMarketIndexToAccountPubkeyMap.get(perpMarketIndex) || ''
677
+ ),
678
+ ]);
679
+ if (this.delistedMarketSetting === DelistedMarketSetting.Discard) {
680
+ this.perpMarketIndexToAccountPubkeyMap.delete(perpMarketIndex);
681
+ }
682
+ }
683
+
684
+ for (const oracle of oracles) {
685
+ await this.oracleMultiSubscriber.removeAccounts([oracle.publicKey]);
686
+ }
687
+ }
688
+
689
+ removeInitialData() {
690
+ this.initialPerpMarketAccountData = new Map();
691
+ this.initialSpotMarketAccountData = new Map();
692
+ this.initialOraclePriceData = new Map();
693
+ }
694
+
410
695
  async unsubscribeFromOracles(): Promise<void> {
411
696
  if (this.oracleMultiSubscriber) {
412
697
  await this.oracleMultiSubscriber.unsubscribe();
413
698
  this.oracleMultiSubscriber = undefined;
414
699
  return;
415
700
  }
416
- await super.unsubscribeFromOracles();
417
701
  }
418
702
 
419
- override async unsubscribe(): Promise<void> {
703
+ async unsubscribe(): Promise<void> {
420
704
  if (this.isSubscribed) {
421
705
  return;
422
706
  }
423
707
 
424
708
  await this.stateAccountSubscriber.unsubscribe();
709
+ await this.unsubscribeFromOracles();
710
+ await this.perpMarketsSubscriber?.unsubscribe();
711
+ await this.spotMarketsSubscriber?.unsubscribe();
425
712
  }
426
713
  }
@@ -1,5 +1,5 @@
1
1
  import { Program } from '@coral-xyz/anchor';
2
- import { Context, PublicKey } from '@solana/web3.js';
2
+ import { Commitment, Context, PublicKey } from '@solana/web3.js';
3
3
  import * as Buffer from 'buffer';
4
4
  import bs58 from 'bs58';
5
5
 
@@ -21,6 +21,21 @@ interface AccountInfoLike {
21
21
  rentEpoch: number;
22
22
  }
23
23
 
24
+ function commitmentLevelToCommitment(
25
+ commitmentLevel: CommitmentLevel
26
+ ): Commitment {
27
+ switch (commitmentLevel) {
28
+ case CommitmentLevel.PROCESSED:
29
+ return 'processed';
30
+ case CommitmentLevel.CONFIRMED:
31
+ return 'confirmed';
32
+ case CommitmentLevel.FINALIZED:
33
+ return 'finalized';
34
+ default:
35
+ return 'confirmed';
36
+ }
37
+ }
38
+
24
39
  export class grpcMultiAccountSubscriber<T> {
25
40
  private client: Client;
26
41
  private stream: ClientDuplexStream<SubscribeRequest, SubscribeUpdate>;
@@ -105,6 +120,56 @@ export class grpcMultiAccountSubscriber<T> {
105
120
  return this.dataMap;
106
121
  }
107
122
 
123
+ async fetch(): Promise<void> {
124
+ try {
125
+ // Chunk account IDs into groups of 100 (getMultipleAccounts limit)
126
+ const chunkSize = 100;
127
+ const chunks: string[][] = [];
128
+ const accountIds = Array.from(this.subscribedAccounts.values());
129
+ for (let i = 0; i < accountIds.length; i += chunkSize) {
130
+ chunks.push(accountIds.slice(i, i + chunkSize));
131
+ }
132
+
133
+ // Process all chunks concurrently
134
+ await Promise.all(
135
+ chunks.map(async (chunk) => {
136
+ const accountAddresses = chunk.map(
137
+ (accountId) => new PublicKey(accountId)
138
+ );
139
+ const rpcResponseAndContext =
140
+ await this.program.provider.connection.getMultipleAccountsInfoAndContext(
141
+ accountAddresses,
142
+ {
143
+ commitment: commitmentLevelToCommitment(this.commitmentLevel),
144
+ }
145
+ );
146
+
147
+ const rpcResponse = rpcResponseAndContext.value;
148
+ const currentSlot = rpcResponseAndContext.context.slot;
149
+
150
+ for (let i = 0; i < chunk.length; i++) {
151
+ const accountId = chunk[i];
152
+ const accountInfo = rpcResponse[i];
153
+ if (accountInfo) {
154
+ const perpMarket = this.program.coder.accounts.decode(
155
+ 'PerpMarket',
156
+ accountInfo.data
157
+ );
158
+ this.setAccountData(accountId, perpMarket, currentSlot);
159
+ }
160
+ }
161
+ })
162
+ );
163
+ } catch (error) {
164
+ if (this.resubOpts?.logResubMessages) {
165
+ console.log(
166
+ `[${this.accountName}] grpcMultiAccountSubscriber error fetching accounts:`,
167
+ error
168
+ );
169
+ }
170
+ }
171
+ }
172
+
108
173
  async subscribe(
109
174
  accounts: PublicKey[],
110
175
  onChange: (