@drift-labs/sdk 2.136.0-beta.0 → 2.136.0-beta.2

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 (77) hide show
  1. package/VERSION +1 -1
  2. package/lib/browser/accounts/types.d.ts +2 -0
  3. package/lib/browser/accounts/webSocketAccountSubscriberV2.d.ts +76 -3
  4. package/lib/browser/accounts/webSocketAccountSubscriberV2.js +211 -39
  5. package/lib/browser/accounts/webSocketDriftClientAccountSubscriberV2.d.ts +87 -0
  6. package/lib/browser/accounts/webSocketDriftClientAccountSubscriberV2.js +444 -0
  7. package/lib/browser/accounts/webSocketProgramAccountsSubscriberV2.d.ts +145 -0
  8. package/lib/browser/accounts/webSocketProgramAccountsSubscriberV2.js +744 -0
  9. package/lib/browser/accounts/websocketProgramUserAccountSubscriber.d.ts +22 -0
  10. package/lib/browser/accounts/websocketProgramUserAccountSubscriber.js +54 -0
  11. package/lib/browser/driftClient.js +22 -18
  12. package/lib/browser/driftClientConfig.d.ts +7 -2
  13. package/lib/browser/factory/bigNum.d.ts +2 -2
  14. package/lib/browser/factory/bigNum.js +20 -5
  15. package/lib/browser/index.d.ts +4 -0
  16. package/lib/browser/index.js +9 -1
  17. package/lib/browser/memcmp.d.ts +2 -0
  18. package/lib/browser/memcmp.js +19 -1
  19. package/lib/browser/oracles/oracleId.d.ts +5 -0
  20. package/lib/browser/oracles/oracleId.js +46 -1
  21. package/lib/browser/user.js +12 -5
  22. package/lib/browser/userConfig.d.ts +3 -0
  23. package/lib/node/accounts/types.d.ts +2 -0
  24. package/lib/node/accounts/types.d.ts.map +1 -1
  25. package/lib/node/accounts/webSocketAccountSubscriberV2.d.ts +76 -3
  26. package/lib/node/accounts/webSocketAccountSubscriberV2.d.ts.map +1 -1
  27. package/lib/node/accounts/webSocketAccountSubscriberV2.js +211 -39
  28. package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.d.ts +88 -0
  29. package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.d.ts.map +1 -0
  30. package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.js +444 -0
  31. package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.d.ts +146 -0
  32. package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.d.ts.map +1 -0
  33. package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.js +744 -0
  34. package/lib/node/accounts/websocketProgramUserAccountSubscriber.d.ts +23 -0
  35. package/lib/node/accounts/websocketProgramUserAccountSubscriber.d.ts.map +1 -0
  36. package/lib/node/accounts/websocketProgramUserAccountSubscriber.js +54 -0
  37. package/lib/node/driftClient.d.ts.map +1 -1
  38. package/lib/node/driftClient.js +22 -18
  39. package/lib/node/driftClientConfig.d.ts +7 -2
  40. package/lib/node/driftClientConfig.d.ts.map +1 -1
  41. package/lib/node/factory/bigNum.d.ts +2 -2
  42. package/lib/node/factory/bigNum.d.ts.map +1 -1
  43. package/lib/node/factory/bigNum.js +20 -5
  44. package/lib/node/index.d.ts +4 -0
  45. package/lib/node/index.d.ts.map +1 -1
  46. package/lib/node/index.js +9 -1
  47. package/lib/node/memcmp.d.ts +2 -0
  48. package/lib/node/memcmp.d.ts.map +1 -1
  49. package/lib/node/memcmp.js +19 -1
  50. package/lib/node/oracles/oracleId.d.ts +5 -0
  51. package/lib/node/oracles/oracleId.d.ts.map +1 -1
  52. package/lib/node/oracles/oracleId.js +46 -1
  53. package/lib/node/user.d.ts.map +1 -1
  54. package/lib/node/user.js +12 -5
  55. package/lib/node/userConfig.d.ts +3 -0
  56. package/lib/node/userConfig.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/accounts/README_WebSocketAccountSubscriberV2.md +41 -0
  59. package/src/accounts/types.ts +3 -0
  60. package/src/accounts/webSocketAccountSubscriberV2.ts +243 -42
  61. package/src/accounts/webSocketDriftClientAccountSubscriberV2.ts +745 -0
  62. package/src/accounts/webSocketProgramAccountsSubscriberV2.ts +995 -0
  63. package/src/accounts/websocketProgramUserAccountSubscriber.ts +94 -0
  64. package/src/driftClient.ts +13 -7
  65. package/src/driftClientConfig.ts +15 -8
  66. package/src/factory/bigNum.ts +22 -5
  67. package/src/index.ts +4 -0
  68. package/src/memcmp.ts +17 -0
  69. package/src/oracles/oracleId.ts +34 -0
  70. package/src/user.ts +21 -9
  71. package/src/userConfig.ts +3 -0
  72. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.d.ts +0 -53
  73. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.js +0 -453
  74. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts +0 -54
  75. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts.map +0 -1
  76. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.js +0 -453
  77. package/src/accounts/webSocketProgramAccountSubscriberV2.ts +0 -596
@@ -0,0 +1,745 @@
1
+ import {
2
+ AccountSubscriber,
3
+ DataAndSlot,
4
+ DelistedMarketSetting,
5
+ DriftClientAccountEvents,
6
+ DriftClientAccountSubscriber,
7
+ NotSubscribedError,
8
+ ResubOpts,
9
+ } from './types';
10
+ import {
11
+ isVariant,
12
+ PerpMarketAccount,
13
+ SpotMarketAccount,
14
+ StateAccount,
15
+ } from '../types';
16
+ import { Program } from '@coral-xyz/anchor';
17
+ import StrictEventEmitter from 'strict-event-emitter-types';
18
+ import { EventEmitter } from 'events';
19
+ import {
20
+ getDriftStateAccountPublicKey,
21
+ getPerpMarketPublicKey,
22
+ getSpotMarketPublicKey,
23
+ } from '../addresses/pda';
24
+ import { Context, PublicKey } from '@solana/web3.js';
25
+ import {
26
+ Commitment,
27
+ SolanaRpcSubscriptionsApi,
28
+ Rpc,
29
+ RpcSubscriptions,
30
+ createSolanaClient,
31
+ } from 'gill';
32
+ import { OracleInfo, OraclePriceData } from '../oracles/types';
33
+ import { OracleClientCache } from '../oracles/oracleClientCache';
34
+ import { QUOTE_ORACLE_PRICE_DATA } from '../oracles/quoteAssetOracleClient';
35
+ import { findAllMarketAndOracles } from '../config';
36
+ import { findDelistedPerpMarketsAndOracles } from './utils';
37
+ import {
38
+ getOracleId,
39
+ getPublicKeyAndSourceFromOracleId,
40
+ } from '../oracles/oracleId';
41
+ import { OracleSource } from '../types';
42
+ import {
43
+ getPerpMarketAccountsFilter,
44
+ getSpotMarketAccountsFilter,
45
+ } from '../memcmp';
46
+ import { WebSocketProgramAccountsSubscriberV2 } from './webSocketProgramAccountsSubscriberV2';
47
+ import { WebSocketAccountSubscriberV2 } from './webSocketAccountSubscriberV2';
48
+ const ORACLE_DEFAULT_ID = getOracleId(
49
+ PublicKey.default,
50
+ OracleSource.QUOTE_ASSET
51
+ );
52
+
53
+ export class WebSocketDriftClientAccountSubscriberV2
54
+ implements DriftClientAccountSubscriber
55
+ {
56
+ isSubscribed: boolean;
57
+ program: Program;
58
+ commitment?: Commitment;
59
+ perpMarketIndexes: number[];
60
+ spotMarketIndexes: number[];
61
+ oracleInfos: OracleInfo[];
62
+ oracleClientCache = new OracleClientCache();
63
+
64
+ resubOpts?: ResubOpts;
65
+ shouldFindAllMarketsAndOracles: boolean;
66
+ skipInitialData: boolean = true;
67
+
68
+ eventEmitter: StrictEventEmitter<EventEmitter, DriftClientAccountEvents>;
69
+ stateAccountSubscriber?: WebSocketAccountSubscriberV2<StateAccount>;
70
+ perpMarketAllAccountsSubscriber: WebSocketProgramAccountsSubscriberV2<PerpMarketAccount>;
71
+ perpMarketAccountLatestData = new Map<
72
+ number,
73
+ DataAndSlot<PerpMarketAccount>
74
+ >();
75
+ spotMarketAllAccountsSubscriber: WebSocketProgramAccountsSubscriberV2<SpotMarketAccount>;
76
+ spotMarketAccountLatestData = new Map<
77
+ number,
78
+ DataAndSlot<SpotMarketAccount>
79
+ >();
80
+ perpOracleMap = new Map<number, PublicKey>();
81
+ perpOracleStringMap = new Map<number, string>();
82
+ spotOracleMap = new Map<number, PublicKey>();
83
+ spotOracleStringMap = new Map<number, string>();
84
+ oracleSubscribers = new Map<string, AccountSubscriber<OraclePriceData>>();
85
+ delistedMarketSetting: DelistedMarketSetting;
86
+
87
+ initialPerpMarketAccountData: Map<number, PerpMarketAccount>;
88
+ initialSpotMarketAccountData: Map<number, SpotMarketAccount>;
89
+ initialOraclePriceData: Map<string, OraclePriceData>;
90
+
91
+ protected isSubscribing = false;
92
+ protected subscriptionPromise: Promise<boolean>;
93
+ protected subscriptionPromiseResolver: (val: boolean) => void;
94
+
95
+ private rpc: Rpc<any>;
96
+ private rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi> &
97
+ string;
98
+
99
+ public constructor(
100
+ program: Program,
101
+ perpMarketIndexes: number[],
102
+ spotMarketIndexes: number[],
103
+ oracleInfos: OracleInfo[],
104
+ shouldFindAllMarketsAndOracles: boolean,
105
+ delistedMarketSetting: DelistedMarketSetting,
106
+ resubOpts?: ResubOpts,
107
+ commitment?: Commitment,
108
+ skipInitialData?: boolean
109
+ ) {
110
+ this.isSubscribed = false;
111
+ this.program = program;
112
+ this.eventEmitter = new EventEmitter();
113
+ this.perpMarketIndexes = perpMarketIndexes;
114
+ this.spotMarketIndexes = spotMarketIndexes;
115
+ this.oracleInfos = oracleInfos;
116
+ this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles;
117
+ this.delistedMarketSetting = delistedMarketSetting;
118
+ this.resubOpts = resubOpts;
119
+ this.commitment = commitment;
120
+ this.skipInitialData = skipInitialData ?? false;
121
+
122
+ const { rpc, rpcSubscriptions } = createSolanaClient({
123
+ urlOrMoniker: this.program.provider.connection.rpcEndpoint,
124
+ });
125
+ this.rpc = rpc;
126
+ this.rpcSubscriptions = rpcSubscriptions;
127
+ }
128
+
129
+ public async subscribe(): Promise<boolean> {
130
+ try {
131
+ const startTime = performance.now();
132
+ if (this.isSubscribed) {
133
+ console.log(
134
+ `[PROFILING] WebSocketDriftClientAccountSubscriberV2.subscribe() skipped - already subscribed`
135
+ );
136
+ return true;
137
+ }
138
+
139
+ if (this.isSubscribing) {
140
+ console.log(
141
+ `[PROFILING] WebSocketDriftClientAccountSubscriberV2.subscribe() waiting for existing subscription`
142
+ );
143
+ return await this.subscriptionPromise;
144
+ }
145
+
146
+ this.isSubscribing = true;
147
+
148
+ // Initialize subscriptionPromiseResolver to a no-op function
149
+ this.subscriptionPromiseResolver = () => {};
150
+
151
+ this.subscriptionPromise = new Promise((res) => {
152
+ this.subscriptionPromiseResolver = res;
153
+ });
154
+
155
+ const [perpMarketAccountPubkeys, spotMarketAccountPubkeys] =
156
+ await Promise.all([
157
+ Promise.all(
158
+ this.perpMarketIndexes.map((marketIndex) =>
159
+ getPerpMarketPublicKey(this.program.programId, marketIndex)
160
+ )
161
+ ),
162
+ Promise.all(
163
+ this.spotMarketIndexes.map((marketIndex) =>
164
+ getSpotMarketPublicKey(this.program.programId, marketIndex)
165
+ )
166
+ ),
167
+ ]);
168
+
169
+ // Profile findAllMarketsAndOracles if needed
170
+ let findAllMarketsDuration = 0;
171
+ if (this.shouldFindAllMarketsAndOracles) {
172
+ const findAllMarketsStartTime = performance.now();
173
+ const {
174
+ perpMarketIndexes,
175
+ perpMarketAccounts,
176
+ spotMarketIndexes,
177
+ spotMarketAccounts,
178
+ oracleInfos,
179
+ } = await findAllMarketAndOracles(this.program);
180
+ this.perpMarketIndexes = perpMarketIndexes;
181
+ this.spotMarketIndexes = spotMarketIndexes;
182
+ this.oracleInfos = oracleInfos;
183
+ // front run and set the initial data here to save extra gma call in set initial data
184
+ this.initialPerpMarketAccountData = new Map(
185
+ perpMarketAccounts.map((market) => [market.marketIndex, market])
186
+ );
187
+ this.initialSpotMarketAccountData = new Map(
188
+ spotMarketAccounts.map((market) => [market.marketIndex, market])
189
+ );
190
+ const findAllMarketsEndTime = performance.now();
191
+ findAllMarketsDuration =
192
+ findAllMarketsEndTime - findAllMarketsStartTime;
193
+ console.log(
194
+ `[PROFILING] findAllMarketAndOracles completed in ${findAllMarketsDuration.toFixed(
195
+ 2
196
+ )}ms (${perpMarketAccounts.length} perp markets, ${
197
+ spotMarketAccounts.length
198
+ } spot markets)`
199
+ );
200
+ }
201
+
202
+ // Create subscribers
203
+ this.perpMarketAllAccountsSubscriber =
204
+ new WebSocketProgramAccountsSubscriberV2<PerpMarketAccount>(
205
+ 'PerpMarketAccountsSubscriber',
206
+ 'PerpMarket',
207
+ this.program,
208
+ this.program.account.perpMarket.coder.accounts.decodeUnchecked.bind(
209
+ this.program.account.perpMarket.coder.accounts
210
+ ),
211
+ {
212
+ filters: [getPerpMarketAccountsFilter()],
213
+ commitment: this.commitment,
214
+ },
215
+ this.resubOpts,
216
+ perpMarketAccountPubkeys // because we pass these in, it will monitor these accounts and fetch them right away
217
+ );
218
+
219
+ this.spotMarketAllAccountsSubscriber =
220
+ new WebSocketProgramAccountsSubscriberV2<SpotMarketAccount>(
221
+ 'SpotMarketAccountsSubscriber',
222
+ 'SpotMarket',
223
+ this.program,
224
+ this.program.account.spotMarket.coder.accounts.decodeUnchecked.bind(
225
+ this.program.account.spotMarket.coder.accounts
226
+ ),
227
+ {
228
+ filters: [getSpotMarketAccountsFilter()],
229
+ commitment: this.commitment,
230
+ },
231
+ this.resubOpts,
232
+ spotMarketAccountPubkeys // because we pass these in, it will monitor these accounts and fetch them right away
233
+ );
234
+
235
+ // Run all subscriptions in parallel
236
+ await Promise.all([
237
+ // Perp market subscription
238
+ this.perpMarketAllAccountsSubscriber.subscribe(
239
+ (
240
+ _accountId: PublicKey,
241
+ data: PerpMarketAccount,
242
+ context: Context,
243
+ _buffer: Buffer
244
+ ) => {
245
+ if (
246
+ this.delistedMarketSetting !== DelistedMarketSetting.Subscribe &&
247
+ isVariant(data.status, 'delisted')
248
+ ) {
249
+ return;
250
+ }
251
+ this.perpMarketAccountLatestData.set(data.marketIndex, {
252
+ data,
253
+ slot: context.slot,
254
+ });
255
+ this.eventEmitter.emit('perpMarketAccountUpdate', data);
256
+ this.eventEmitter.emit('update');
257
+ }
258
+ ),
259
+ // Spot market subscription
260
+ this.spotMarketAllAccountsSubscriber.subscribe(
261
+ (
262
+ _accountId: PublicKey,
263
+ data: SpotMarketAccount,
264
+ context: Context,
265
+ _buffer: Buffer
266
+ ) => {
267
+ if (
268
+ this.delistedMarketSetting !== DelistedMarketSetting.Subscribe &&
269
+ isVariant(data.status, 'delisted')
270
+ ) {
271
+ return;
272
+ }
273
+ this.spotMarketAccountLatestData.set(data.marketIndex, {
274
+ data,
275
+ slot: context.slot,
276
+ });
277
+ this.eventEmitter.emit('spotMarketAccountUpdate', data);
278
+ this.eventEmitter.emit('update');
279
+ }
280
+ ),
281
+ // State account subscription
282
+ (async () => {
283
+ const statePublicKey = await getDriftStateAccountPublicKey(
284
+ this.program.programId
285
+ );
286
+ this.stateAccountSubscriber = new WebSocketAccountSubscriberV2(
287
+ 'state',
288
+ this.program,
289
+ statePublicKey,
290
+ undefined,
291
+ undefined,
292
+ this.commitment as Commitment,
293
+ this.rpcSubscriptions,
294
+ this.rpc
295
+ );
296
+ await Promise.all([
297
+ this.stateAccountSubscriber.fetch(),
298
+ this.stateAccountSubscriber.subscribe((data: StateAccount) => {
299
+ this.eventEmitter.emit('stateAccountUpdate', data);
300
+ this.eventEmitter.emit('update');
301
+ }),
302
+ ]);
303
+ })(),
304
+ (async () => {
305
+ await this.setInitialData();
306
+ const subscribeToOraclesStartTime = performance.now();
307
+ await this.subscribeToOracles();
308
+ const subscribeToOraclesEndTime = performance.now();
309
+ const duration =
310
+ subscribeToOraclesEndTime - subscribeToOraclesStartTime;
311
+ return duration;
312
+ })(),
313
+ ]);
314
+
315
+ // const initialPerpMarketDataFromLatestData = new Map(
316
+ // Array.from(this.perpMarketAccountLatestData.values()).map((data) => [
317
+ // data.data.marketIndex,
318
+ // data.data,
319
+ // ])
320
+ // );
321
+ // const initialSpotMarketDataFromLatestData = new Map(
322
+ // Array.from(this.spotMarketAccountLatestData.values()).map((data) => [
323
+ // data.data.marketIndex,
324
+ // data.data,
325
+ // ])
326
+ // );
327
+ // this.initialPerpMarketAccountData = initialPerpMarketDataFromLatestData;
328
+ // this.initialSpotMarketAccountData = initialSpotMarketDataFromLatestData;
329
+
330
+ await this.handleDelistedMarketOracles();
331
+
332
+ await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]);
333
+
334
+ this.eventEmitter.emit('update');
335
+ // delete initial data
336
+ this.removeInitialData();
337
+
338
+ const totalDuration = performance.now() - startTime;
339
+ console.log(
340
+ `[PROFILING] WebSocketDriftClientAccountSubscriberV2.subscribe() completed in ${totalDuration.toFixed(
341
+ 2
342
+ )}ms`
343
+ );
344
+
345
+ // Resolve the subscription promise
346
+ this.isSubscribed = true;
347
+ this.isSubscribing = false;
348
+ // Before calling subscriptionPromiseResolver, check if it's defined
349
+ if (this.subscriptionPromiseResolver) {
350
+ this.subscriptionPromiseResolver(true);
351
+ }
352
+
353
+ return true;
354
+ } catch (error) {
355
+ console.error('Subscription failed:', error);
356
+ this.isSubscribing = false;
357
+ this.subscriptionPromiseResolver(false);
358
+ return false;
359
+ }
360
+ }
361
+
362
+ chunks = <T>(array: readonly T[], size: number): T[][] => {
363
+ const result: T[][] = [];
364
+ for (let i = 0; i < array.length; i += size) {
365
+ result.push(array.slice(i, i + size));
366
+ }
367
+ return result;
368
+ };
369
+
370
+ public async fetch(): Promise<void> {
371
+ await this.setInitialData();
372
+ }
373
+
374
+ /**
375
+ * This is a no-op method that always returns true.
376
+ * Unlike the previous implementation, we don't need to manually subscribe to individual perp markets
377
+ * because we automatically receive updates for all program account changes via a single websocket subscription.
378
+ * This means any new perp markets will automatically be included without explicit subscription.
379
+ * @param marketIndex The perp market index to add (unused)
380
+ * @returns Promise that resolves to true
381
+ */
382
+ public addPerpMarket(_marketIndex: number): Promise<boolean> {
383
+ return Promise.resolve(true);
384
+ }
385
+
386
+ /**
387
+ * This is a no-op method that always returns true.
388
+ * Unlike the previous implementation, we don't need to manually subscribe to individual spot markets
389
+ * because we automatically receive updates for all program account changes via a single websocket subscription.
390
+ * This means any new spot markets will automatically be included without explicit subscription.
391
+ * @param marketIndex The spot market index to add (unused)
392
+ * @returns Promise that resolves to true
393
+ */
394
+ public addSpotMarket(_marketIndex: number): Promise<boolean> {
395
+ return Promise.resolve(true);
396
+ }
397
+
398
+ // TODO: need more options to skip loading perp market and spot market data. Because of how we fetch within the program account subscribers, I am commenting this all out
399
+ async setInitialData(): Promise<void> {
400
+ const connection = this.program.provider.connection;
401
+ // Profile oracle initial data setup
402
+ const oracleSetupStartTime = performance.now();
403
+ const oracleAccountPubkeyChunks = this.chunks(
404
+ this.oracleInfos.map((oracleInfo) => oracleInfo.publicKey),
405
+ 100
406
+ );
407
+ const oracleAccountInfos = (
408
+ await Promise.all(
409
+ oracleAccountPubkeyChunks.map((oracleAccountPublicKeysChunk) =>
410
+ connection.getMultipleAccountsInfo(oracleAccountPublicKeysChunk)
411
+ )
412
+ )
413
+ ).flat();
414
+ this.initialOraclePriceData = new Map(
415
+ this.oracleInfos.reduce((result, oracleInfo, i) => {
416
+ if (!oracleAccountInfos[i]) {
417
+ return result;
418
+ }
419
+
420
+ const oracleClient = this.oracleClientCache.get(
421
+ oracleInfo.source,
422
+ connection,
423
+ this.program
424
+ );
425
+
426
+ const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer(
427
+ oracleAccountInfos[i].data
428
+ );
429
+
430
+ result.push([
431
+ getOracleId(oracleInfo.publicKey, oracleInfo.source),
432
+ oraclePriceData,
433
+ ]);
434
+ return result;
435
+ }, [])
436
+ );
437
+ const oracleSetupEndTime = performance.now();
438
+ const oracleSetupDuration = oracleSetupEndTime - oracleSetupStartTime;
439
+ if (this.resubOpts?.logResubMessages) {
440
+ console.log(
441
+ `[PROFILING] Oracle initial data setup completed in ${oracleSetupDuration.toFixed(
442
+ 2
443
+ )}ms (${this.initialOraclePriceData.size} oracles)`
444
+ );
445
+ }
446
+
447
+ // emit initial oracle price data
448
+ Array.from(this.initialOraclePriceData.entries()).forEach(
449
+ ([oracleId, oraclePriceData]) => {
450
+ const { publicKey, source } =
451
+ getPublicKeyAndSourceFromOracleId(oracleId);
452
+ this.eventEmitter.emit(
453
+ 'oraclePriceUpdate',
454
+ publicKey,
455
+ source,
456
+ oraclePriceData
457
+ );
458
+ }
459
+ );
460
+ this.eventEmitter.emit('update');
461
+ }
462
+
463
+ removeInitialData() {
464
+ this.initialPerpMarketAccountData = new Map();
465
+ this.initialSpotMarketAccountData = new Map();
466
+ this.initialOraclePriceData = new Map();
467
+ }
468
+
469
+ async subscribeToOracles(): Promise<boolean> {
470
+ const startTime = performance.now();
471
+
472
+ // Filter out default oracles and duplicates to avoid unnecessary subscriptions
473
+ const validOracleInfos = this.oracleInfos.filter(
474
+ (oracleInfo) =>
475
+ !this.oracleSubscribers.has(
476
+ getOracleId(oracleInfo.publicKey, oracleInfo.source)
477
+ )
478
+ );
479
+
480
+ await Promise.all(
481
+ validOracleInfos.map((oracleInfo) => this.subscribeToOracle(oracleInfo))
482
+ );
483
+
484
+ const totalDuration = performance.now() - startTime;
485
+ console.log(
486
+ `[PROFILING] subscribeToOracles() completed in ${totalDuration.toFixed(
487
+ 2
488
+ )}ms`
489
+ );
490
+
491
+ return true;
492
+ }
493
+
494
+ async subscribeToOracle(oracleInfo: OracleInfo): Promise<boolean> {
495
+ try {
496
+ const oracleId = getOracleId(oracleInfo.publicKey, oracleInfo.source);
497
+
498
+ const client = this.oracleClientCache.get(
499
+ oracleInfo.source,
500
+ this.program.provider.connection,
501
+ this.program
502
+ );
503
+ const accountSubscriber =
504
+ new WebSocketAccountSubscriberV2<OraclePriceData>(
505
+ 'oracle',
506
+ this.program,
507
+ oracleInfo.publicKey,
508
+ (buffer: Buffer) => {
509
+ return client.getOraclePriceDataFromBuffer(buffer);
510
+ },
511
+ this.resubOpts,
512
+ this.commitment,
513
+ this.rpcSubscriptions,
514
+ this.rpc
515
+ );
516
+ const initialOraclePriceData = this.initialOraclePriceData?.get(oracleId);
517
+ if (initialOraclePriceData) {
518
+ accountSubscriber.setData(initialOraclePriceData);
519
+ }
520
+ await accountSubscriber.subscribe((data: OraclePriceData) => {
521
+ this.eventEmitter.emit(
522
+ 'oraclePriceUpdate',
523
+ oracleInfo.publicKey,
524
+ oracleInfo.source,
525
+ data
526
+ );
527
+ this.eventEmitter.emit('update');
528
+ });
529
+
530
+ this.oracleSubscribers.set(oracleId, accountSubscriber);
531
+
532
+ return true;
533
+ } catch (error) {
534
+ console.error(
535
+ `Failed to subscribe to oracle ${oracleInfo.publicKey.toString()}:`,
536
+ error
537
+ );
538
+ return false;
539
+ }
540
+ }
541
+
542
+ async unsubscribeFromMarketAccounts(): Promise<void> {
543
+ await this.perpMarketAllAccountsSubscriber.unsubscribe();
544
+ }
545
+
546
+ async unsubscribeFromSpotMarketAccounts(): Promise<void> {
547
+ await this.spotMarketAllAccountsSubscriber.unsubscribe();
548
+ }
549
+
550
+ async unsubscribeFromOracles(): Promise<void> {
551
+ await Promise.all(
552
+ Array.from(this.oracleSubscribers.values()).map((accountSubscriber) =>
553
+ accountSubscriber.unsubscribe()
554
+ )
555
+ );
556
+ }
557
+
558
+ public async unsubscribe(): Promise<void> {
559
+ if (!this.isSubscribed) {
560
+ return;
561
+ }
562
+
563
+ if (this.subscriptionPromise) {
564
+ await this.subscriptionPromise;
565
+ }
566
+ await Promise.all([
567
+ this.stateAccountSubscriber?.unsubscribe(),
568
+ this.unsubscribeFromMarketAccounts(),
569
+ this.unsubscribeFromSpotMarketAccounts(),
570
+ this.unsubscribeFromOracles(),
571
+ ]);
572
+
573
+ this.isSubscribed = false;
574
+ this.isSubscribing = false;
575
+ this.subscriptionPromiseResolver = () => {};
576
+ }
577
+
578
+ async addOracle(oracleInfo: OracleInfo): Promise<boolean> {
579
+ const oracleId = getOracleId(oracleInfo.publicKey, oracleInfo.source);
580
+ if (this.oracleSubscribers.has(oracleId)) {
581
+ return true;
582
+ }
583
+
584
+ if (oracleInfo.publicKey.equals(PublicKey.default)) {
585
+ return true;
586
+ }
587
+
588
+ return this.subscribeToOracle(oracleInfo);
589
+ }
590
+
591
+ async setPerpOracleMap() {
592
+ const perpMarkets = this.getMarketAccountsAndSlots();
593
+ const addOraclePromises = [];
594
+ for (const perpMarket of perpMarkets) {
595
+ if (!perpMarket || !perpMarket.data) {
596
+ continue;
597
+ }
598
+ const perpMarketAccount = perpMarket.data;
599
+ const perpMarketIndex = perpMarketAccount.marketIndex;
600
+ const oracle = perpMarketAccount.amm.oracle;
601
+ const oracleId = getOracleId(oracle, perpMarket.data.amm.oracleSource);
602
+ if (!this.oracleSubscribers.has(oracleId)) {
603
+ addOraclePromises.push(
604
+ this.addOracle({
605
+ publicKey: oracle,
606
+ source: perpMarket.data.amm.oracleSource,
607
+ })
608
+ );
609
+ }
610
+ this.perpOracleMap.set(perpMarketIndex, oracle);
611
+ this.perpOracleStringMap.set(perpMarketIndex, oracleId);
612
+ }
613
+ await Promise.all(addOraclePromises);
614
+ }
615
+
616
+ async setSpotOracleMap() {
617
+ const spotMarkets = this.getSpotMarketAccountsAndSlots();
618
+ const addOraclePromises = [];
619
+ for (const spotMarket of spotMarkets) {
620
+ if (!spotMarket || !spotMarket.data) {
621
+ continue;
622
+ }
623
+ const spotMarketAccount = spotMarket.data;
624
+ const spotMarketIndex = spotMarketAccount.marketIndex;
625
+ const oracle = spotMarketAccount.oracle;
626
+ const oracleId = getOracleId(oracle, spotMarketAccount.oracleSource);
627
+ if (!this.oracleSubscribers.has(oracleId)) {
628
+ addOraclePromises.push(
629
+ this.addOracle({
630
+ publicKey: oracle,
631
+ source: spotMarketAccount.oracleSource,
632
+ })
633
+ );
634
+ }
635
+ this.spotOracleMap.set(spotMarketIndex, oracle);
636
+ this.spotOracleStringMap.set(spotMarketIndex, oracleId);
637
+ }
638
+ await Promise.all(addOraclePromises);
639
+ }
640
+
641
+ async handleDelistedMarketOracles(): Promise<void> {
642
+ if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) {
643
+ return;
644
+ }
645
+
646
+ const { oracles } = findDelistedPerpMarketsAndOracles(
647
+ this.getMarketAccountsAndSlots(),
648
+ this.getSpotMarketAccountsAndSlots()
649
+ );
650
+
651
+ for (const oracle of oracles) {
652
+ const oracleId = getOracleId(oracle.publicKey, oracle.source);
653
+ if (this.oracleSubscribers.has(oracleId)) {
654
+ await this.oracleSubscribers.get(oracleId).unsubscribe();
655
+ if (this.delistedMarketSetting === DelistedMarketSetting.Discard) {
656
+ this.oracleSubscribers.delete(oracleId);
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ assertIsSubscribed(): void {
663
+ if (!this.isSubscribed) {
664
+ throw new NotSubscribedError(
665
+ 'You must call `subscribe` before using this function'
666
+ );
667
+ }
668
+ }
669
+
670
+ public getStateAccountAndSlot(): DataAndSlot<StateAccount> {
671
+ this.assertIsSubscribed();
672
+ return this.stateAccountSubscriber.dataAndSlot;
673
+ }
674
+
675
+ public getMarketAccountAndSlot(
676
+ marketIndex: number
677
+ ): DataAndSlot<PerpMarketAccount> | undefined {
678
+ this.assertIsSubscribed();
679
+ return this.perpMarketAccountLatestData.get(marketIndex);
680
+ }
681
+
682
+ public getMarketAccountsAndSlots(): DataAndSlot<PerpMarketAccount>[] {
683
+ return Array.from(this.perpMarketAccountLatestData.values());
684
+ }
685
+
686
+ public getSpotMarketAccountAndSlot(
687
+ marketIndex: number
688
+ ): DataAndSlot<SpotMarketAccount> | undefined {
689
+ this.assertIsSubscribed();
690
+ return this.spotMarketAccountLatestData.get(marketIndex);
691
+ }
692
+
693
+ public getSpotMarketAccountsAndSlots(): DataAndSlot<SpotMarketAccount>[] {
694
+ return Array.from(this.spotMarketAccountLatestData.values());
695
+ }
696
+
697
+ public getOraclePriceDataAndSlot(
698
+ oracleId: string
699
+ ): DataAndSlot<OraclePriceData> | undefined {
700
+ this.assertIsSubscribed();
701
+ if (oracleId === ORACLE_DEFAULT_ID) {
702
+ return {
703
+ data: QUOTE_ORACLE_PRICE_DATA,
704
+ slot: 0,
705
+ };
706
+ }
707
+ return this.oracleSubscribers.get(oracleId)?.dataAndSlot;
708
+ }
709
+
710
+ public getOraclePriceDataAndSlotForPerpMarket(
711
+ marketIndex: number
712
+ ): DataAndSlot<OraclePriceData> | undefined {
713
+ const perpMarketAccount = this.getMarketAccountAndSlot(marketIndex);
714
+ const oracle = this.perpOracleMap.get(marketIndex);
715
+ const oracleId = this.perpOracleStringMap.get(marketIndex);
716
+ if (!perpMarketAccount || !oracleId) {
717
+ return undefined;
718
+ }
719
+
720
+ if (!perpMarketAccount.data.amm.oracle.equals(oracle)) {
721
+ // If the oracle has changed, we need to update the oracle map in background
722
+ this.setPerpOracleMap();
723
+ }
724
+
725
+ return this.getOraclePriceDataAndSlot(oracleId);
726
+ }
727
+
728
+ public getOraclePriceDataAndSlotForSpotMarket(
729
+ marketIndex: number
730
+ ): DataAndSlot<OraclePriceData> | undefined {
731
+ const spotMarketAccount = this.getSpotMarketAccountAndSlot(marketIndex);
732
+ const oracle = this.spotOracleMap.get(marketIndex);
733
+ const oracleId = this.spotOracleStringMap.get(marketIndex);
734
+ if (!spotMarketAccount || !oracleId) {
735
+ return undefined;
736
+ }
737
+
738
+ if (!spotMarketAccount.data.oracle.equals(oracle)) {
739
+ // If the oracle has changed, we need to update the oracle map in background
740
+ this.setSpotOracleMap();
741
+ }
742
+
743
+ return this.getOraclePriceDataAndSlot(oracleId);
744
+ }
745
+ }