@drift-labs/sdk 2.48.0-beta.2 → 2.48.0-beta.21

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 (53) hide show
  1. package/VERSION +1 -1
  2. package/lib/accounts/bulkAccountLoader.js +1 -1
  3. package/lib/accounts/pollingUserAccountSubscriber.js +14 -7
  4. package/lib/accounts/pollingUserStatsAccountSubscriber.js +14 -7
  5. package/lib/accounts/webSocketAccountSubscriber.js +1 -1
  6. package/lib/accounts/webSocketDriftClientAccountSubscriber.d.ts +3 -2
  7. package/lib/accounts/webSocketDriftClientAccountSubscriber.js +6 -5
  8. package/lib/accounts/webSocketProgramAccountSubscriber.js +1 -1
  9. package/lib/accounts/webSocketUserAccountSubscriber.js +1 -1
  10. package/lib/constants/spotMarkets.js +10 -0
  11. package/lib/dlob/orderBookLevels.js +1 -1
  12. package/lib/driftClient.js +4 -4
  13. package/lib/events/eventSubscriber.js +23 -2
  14. package/lib/events/pollingLogProvider.d.ts +1 -1
  15. package/lib/events/pollingLogProvider.js +1 -1
  16. package/lib/events/types.d.ts +9 -2
  17. package/lib/events/webSocketLogProvider.d.ts +15 -3
  18. package/lib/events/webSocketLogProvider.js +66 -7
  19. package/lib/orderSubscriber/OrderSubscriber.d.ts +5 -1
  20. package/lib/orderSubscriber/OrderSubscriber.js +24 -7
  21. package/lib/orderSubscriber/WebsocketSubscription.d.ts +4 -1
  22. package/lib/orderSubscriber/WebsocketSubscription.js +4 -3
  23. package/lib/orderSubscriber/types.d.ts +3 -1
  24. package/lib/slot/SlotSubscriber.js +4 -2
  25. package/lib/user.js +3 -3
  26. package/lib/userMap/userMap.d.ts +16 -4
  27. package/lib/userMap/userMap.js +83 -41
  28. package/lib/userMap/userStatsMap.d.ts +29 -8
  29. package/lib/userMap/userStatsMap.js +46 -41
  30. package/package.json +1 -1
  31. package/src/accounts/bulkAccountLoader.ts +1 -1
  32. package/src/accounts/pollingUserAccountSubscriber.ts +19 -11
  33. package/src/accounts/pollingUserStatsAccountSubscriber.ts +20 -12
  34. package/src/accounts/webSocketAccountSubscriber.ts +1 -1
  35. package/src/accounts/webSocketDriftClientAccountSubscriber.ts +13 -6
  36. package/src/accounts/webSocketProgramAccountSubscriber.ts +1 -1
  37. package/src/accounts/webSocketUserAccountSubscriber.ts +1 -1
  38. package/src/constants/spotMarkets.ts +10 -0
  39. package/src/dlob/orderBookLevels.ts +1 -1
  40. package/src/driftClient.ts +3 -2
  41. package/src/events/eventSubscriber.ts +46 -2
  42. package/src/events/pollingLogProvider.ts +2 -2
  43. package/src/events/types.ts +11 -2
  44. package/src/events/webSocketLogProvider.ts +78 -8
  45. package/src/orderSubscriber/OrderSubscriber.ts +39 -15
  46. package/src/orderSubscriber/WebsocketSubscription.ts +7 -2
  47. package/src/orderSubscriber/types.ts +3 -1
  48. package/src/slot/SlotSubscriber.ts +4 -2
  49. package/src/user.ts +3 -3
  50. package/src/userMap/userMap.ts +139 -66
  51. package/src/userMap/userStatsMap.ts +64 -69
  52. package/tests/amm/test.ts +3 -2
  53. package/tests/dlob/test.ts +8 -5
@@ -150,6 +150,16 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [
150
150
  '2sTMN9A1D1qeZLF95XQgJCUPiKe5DiV52jLfZGqMP46m'
151
151
  ),
152
152
  },
153
+ {
154
+ symbol: 'bSOL',
155
+ marketIndex: 8,
156
+ oracle: new PublicKey('AFrYBhb5wKQtxRS9UA9YRS4V3dwFm7SqmS6DHKq6YVgo'),
157
+ oracleSource: OracleSource.PYTH,
158
+ mint: new PublicKey('bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1'),
159
+ precision: new BN(10).pow(NINE),
160
+ precisionExp: NINE,
161
+ serumMarket: new PublicKey('ARjaHVxGCQfTvvKjLd7U7srvk6orthZSE6uqWchCczZc'),
162
+ },
153
163
  ];
154
164
 
155
165
  export const SpotMarkets: { [key in DriftEnv]: SpotMarketConfig[] } = {
@@ -173,7 +173,7 @@ export function getVammL2Generator({
173
173
  updatedAmm.orderStepSize
174
174
  );
175
175
 
176
- const minOrderSize = marketAccount.amm.orderStepSize;
176
+ const minOrderSize = marketAccount.amm.minOrderSize;
177
177
  if (openBids.lt(minOrderSize.muln(2))) {
178
178
  openBids = ZERO;
179
179
  }
@@ -283,7 +283,8 @@ export class DriftClient {
283
283
  config.spotMarketIndexes ?? [],
284
284
  config.oracleInfos ?? [],
285
285
  noMarketsAndOraclesSpecified,
286
- config.accountSubscription?.resubTimeoutMs
286
+ config.accountSubscription?.resubTimeoutMs,
287
+ config.accountSubscription?.commitment
287
288
  );
288
289
  }
289
290
  this.eventEmitter = this.accountSubscriber.eventEmitter;
@@ -2028,7 +2029,7 @@ export class DriftClient {
2028
2029
  }
2029
2030
 
2030
2031
  const tx = await this.buildTransaction(withdrawIxs, {
2031
- computeUnits: 600_000,
2032
+ computeUnits: 1_400_000,
2032
2033
  });
2033
2034
  const { txSig, slot } = await this.sendTransaction(
2034
2035
  tx,
@@ -8,6 +8,7 @@ import {
8
8
  EventMap,
9
9
  LogProvider,
10
10
  EventSubscriberEvents,
11
+ WebSocketLogProviderConfig,
11
12
  } from './types';
12
13
  import { TxEventCache } from './txEventCache';
13
14
  import { EventList } from './eventList';
@@ -56,7 +57,8 @@ export class EventSubscriber {
56
57
  this.logProvider = new WebSocketLogProvider(
57
58
  this.connection,
58
59
  this.address,
59
- this.options.commitment
60
+ this.options.commitment,
61
+ this.options.logProviderConfig.resubTimeoutMs
60
62
  );
61
63
  } else {
62
64
  this.logProvider = new PollingLogProvider(
@@ -75,6 +77,48 @@ export class EventSubscriber {
75
77
  return true;
76
78
  }
77
79
 
80
+ if (this.options.logProviderConfig.type === 'websocket') {
81
+ if (this.options.logProviderConfig.resubTimeoutMs) {
82
+ if (
83
+ this.options.logProviderConfig.maxReconnectAttempts &&
84
+ this.options.logProviderConfig.maxReconnectAttempts > 0
85
+ ) {
86
+ const logProviderConfig = this.options
87
+ .logProviderConfig as WebSocketLogProviderConfig;
88
+ this.logProvider.eventEmitter.on(
89
+ 'reconnect',
90
+ (reconnectAttempts) => {
91
+ if (
92
+ reconnectAttempts > logProviderConfig.maxReconnectAttempts
93
+ ) {
94
+ console.log('Failing over to polling');
95
+ this.logProvider.eventEmitter.removeAllListeners('reconnect');
96
+ this.unsubscribe().then(() => {
97
+ this.logProvider = new PollingLogProvider(
98
+ this.connection,
99
+ this.address,
100
+ this.options.commitment,
101
+ logProviderConfig.fallbackFrequency,
102
+ logProviderConfig.fallbackBatchSize
103
+ );
104
+ this.logProvider.subscribe(
105
+ (txSig, slot, logs, mostRecentBlockTime) => {
106
+ this.handleTxLogs(
107
+ txSig,
108
+ slot,
109
+ logs,
110
+ mostRecentBlockTime
111
+ );
112
+ },
113
+ true
114
+ );
115
+ });
116
+ }
117
+ }
118
+ );
119
+ }
120
+ }
121
+ }
78
122
  this.logProvider.subscribe((txSig, slot, logs, mostRecentBlockTime) => {
79
123
  this.handleTxLogs(txSig, slot, logs, mostRecentBlockTime);
80
124
  }, true);
@@ -158,7 +202,7 @@ export class EventSubscriber {
158
202
  }
159
203
 
160
204
  public async unsubscribe(): Promise<boolean> {
161
- return await this.logProvider.unsubscribe();
205
+ return await this.logProvider.unsubscribe(true);
162
206
  }
163
207
 
164
208
  private parseEventsFromLogs(
@@ -25,10 +25,10 @@ export class PollingLogProvider implements LogProvider {
25
25
  this.finality = commitment === 'finalized' ? 'finalized' : 'confirmed';
26
26
  }
27
27
 
28
- public subscribe(
28
+ public async subscribe(
29
29
  callback: logProviderCallback,
30
30
  skipHistory?: boolean
31
- ): boolean {
31
+ ): Promise<boolean> {
32
32
  if (this.intervalId) {
33
33
  return true;
34
34
  }
@@ -15,6 +15,7 @@ import {
15
15
  CurveRecord,
16
16
  SwapRecord,
17
17
  } from '../index';
18
+ import { EventEmitter } from 'events';
18
19
 
19
20
  export type EventSubscriptionOptions = {
20
21
  address?: PublicKey;
@@ -126,12 +127,20 @@ export type logProviderCallback = (
126
127
 
127
128
  export interface LogProvider {
128
129
  isSubscribed(): boolean;
129
- subscribe(callback: logProviderCallback, skipHistory?: boolean): boolean;
130
- unsubscribe(): Promise<boolean>;
130
+ subscribe(
131
+ callback: logProviderCallback,
132
+ skipHistory?: boolean
133
+ ): Promise<boolean>;
134
+ unsubscribe(external: boolean): Promise<boolean>;
135
+ eventEmitter?: EventEmitter;
131
136
  }
132
137
 
133
138
  export type WebSocketLogProviderConfig = {
134
139
  type: 'websocket';
140
+ resubTimeoutMs?: number;
141
+ maxReconnectAttempts?: number;
142
+ fallbackFrequency?: number;
143
+ fallbackBatchSize?: number;
135
144
  };
136
145
 
137
146
  export type PollingLogProviderConfig = {
@@ -1,38 +1,108 @@
1
1
  import { LogProvider, logProviderCallback } from './types';
2
2
  import { Commitment, Connection, PublicKey } from '@solana/web3.js';
3
+ import { EventEmitter } from 'events';
3
4
 
4
5
  export class WebSocketLogProvider implements LogProvider {
5
6
  private subscriptionId: number;
7
+ private isUnsubscribing = false;
8
+ private externalUnsubscribe = false;
9
+ private receivingData = false;
10
+ private timeoutId?: NodeJS.Timeout;
11
+ private reconnectAttempts = 0;
12
+ eventEmitter?: EventEmitter;
13
+ private callback?: logProviderCallback;
6
14
  public constructor(
7
15
  private connection: Connection,
8
16
  private address: PublicKey,
9
- private commitment: Commitment
10
- ) {}
17
+ private commitment: Commitment,
18
+ private resubTimeoutMs?: number
19
+ ) {
20
+ if (this.resubTimeoutMs) {
21
+ this.eventEmitter = new EventEmitter();
22
+ }
23
+ }
11
24
 
12
- public subscribe(callback: logProviderCallback): boolean {
25
+ public async subscribe(callback: logProviderCallback): Promise<boolean> {
13
26
  if (this.subscriptionId) {
14
27
  return true;
15
28
  }
16
29
 
30
+ this.callback = callback;
31
+ try {
32
+ this.setSubscription(callback);
33
+ } catch (error) {
34
+ // Sometimes ws connection isn't ready, give it a few secs
35
+ setTimeout(() => this.setSubscription(callback), 2000);
36
+ }
37
+
38
+ if (this.resubTimeoutMs) {
39
+ this.setTimeout();
40
+ }
41
+
42
+ return true;
43
+ }
44
+
45
+ public setSubscription(callback: logProviderCallback): void {
17
46
  this.subscriptionId = this.connection.onLogs(
18
47
  this.address,
19
48
  (logs, ctx) => {
49
+ if (this.resubTimeoutMs && !this.isUnsubscribing) {
50
+ this.receivingData = true;
51
+ clearTimeout(this.timeoutId);
52
+ this.setTimeout();
53
+ }
20
54
  callback(logs.signature, ctx.slot, logs.logs, undefined);
21
55
  },
22
56
  this.commitment
23
57
  );
24
- return true;
25
58
  }
26
59
 
27
60
  public isSubscribed(): boolean {
28
61
  return this.subscriptionId !== undefined;
29
62
  }
30
63
 
31
- public async unsubscribe(): Promise<boolean> {
64
+ public async unsubscribe(external = false): Promise<boolean> {
65
+ this.isUnsubscribing = true;
66
+ this.externalUnsubscribe = external;
67
+ clearTimeout(this.timeoutId);
68
+ this.timeoutId = undefined;
69
+
32
70
  if (this.subscriptionId !== undefined) {
33
- await this.connection.removeOnLogsListener(this.subscriptionId);
34
- this.subscriptionId = undefined;
71
+ try {
72
+ await this.connection.removeOnLogsListener(this.subscriptionId);
73
+ this.subscriptionId = undefined;
74
+ this.isUnsubscribing = false;
75
+ return true;
76
+ } catch (err) {
77
+ console.log('Error unsubscribing from logs: ', err);
78
+ this.isUnsubscribing = false;
79
+ return false;
80
+ }
81
+ } else {
82
+ this.isUnsubscribing = false;
83
+ return true;
35
84
  }
36
- return true;
85
+ }
86
+
87
+ private setTimeout(): void {
88
+ this.timeoutId = setTimeout(async () => {
89
+ if (this.isUnsubscribing || this.externalUnsubscribe) {
90
+ // If we are in the process of unsubscribing, do not attempt to resubscribe
91
+ return;
92
+ }
93
+
94
+ if (this.receivingData) {
95
+ console.log(
96
+ `No log data in ${this.resubTimeoutMs}ms, resubscribing on attempt ${
97
+ this.reconnectAttempts + 1
98
+ }`
99
+ );
100
+ await this.unsubscribe();
101
+ this.receivingData = false;
102
+ this.reconnectAttempts++;
103
+ this.eventEmitter.emit('reconnect', this.reconnectAttempts);
104
+ this.subscribe(this.callback);
105
+ }
106
+ }, this.resubTimeoutMs);
37
107
  }
38
108
  }
@@ -1,7 +1,7 @@
1
1
  import { DriftClient } from '../driftClient';
2
2
  import { UserAccount } from '../types';
3
3
  import { getUserFilter, getUserWithOrderFilter } from '../memcmp';
4
- import { PublicKey, RpcResponseAndContext } from '@solana/web3.js';
4
+ import { Commitment, PublicKey, RpcResponseAndContext } from '@solana/web3.js';
5
5
  import { Buffer } from 'buffer';
6
6
  import { DLOB } from '../dlob/DLOB';
7
7
  import { OrderSubscriberConfig, OrderSubscriberEvents } from './types';
@@ -14,13 +14,17 @@ export class OrderSubscriber {
14
14
  driftClient: DriftClient;
15
15
  usersAccounts = new Map<string, { slot: number; userAccount: UserAccount }>();
16
16
  subscription: PollingSubscription | WebsocketSubscription;
17
+ commitment: Commitment;
17
18
  eventEmitter: StrictEventEmitter<EventEmitter, OrderSubscriberEvents>;
18
19
 
19
20
  fetchPromise?: Promise<void>;
20
21
  fetchPromiseResolver: () => void;
21
22
 
23
+ mostRecentSlot: number;
24
+
22
25
  constructor(config: OrderSubscriberConfig) {
23
26
  this.driftClient = config.driftClient;
27
+ this.commitment = config.subscriptionConfig.commitment || 'processed';
24
28
  if (config.subscriptionConfig.type === 'polling') {
25
29
  this.subscription = new PollingSubscription({
26
30
  orderSubscriber: this,
@@ -29,6 +33,7 @@ export class OrderSubscriber {
29
33
  } else {
30
34
  this.subscription = new WebsocketSubscription({
31
35
  orderSubscriber: this,
36
+ commitment: this.commitment,
32
37
  skipInitialLoad: config.subscriptionConfig.skipInitialLoad,
33
38
  resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs,
34
39
  });
@@ -53,7 +58,7 @@ export class OrderSubscriber {
53
58
  const rpcRequestArgs = [
54
59
  this.driftClient.program.programId.toBase58(),
55
60
  {
56
- commitment: this.driftClient.opts.commitment,
61
+ commitment: this.commitment,
57
62
  filters: [getUserFilter(), getUserWithOrderFilter()],
58
63
  encoding: 'base64',
59
64
  withContext: true,
@@ -81,18 +86,13 @@ export class OrderSubscriber {
81
86
  const programAccountSet = new Set<string>();
82
87
  for (const programAccount of rpcResponseAndContext.value) {
83
88
  const key = programAccount.pubkey.toString();
84
- // @ts-ignore
85
- const buffer = Buffer.from(
86
- programAccount.account.data[0],
87
- programAccount.account.data[1]
88
- );
89
89
  programAccountSet.add(key);
90
- const userAccount =
91
- this.driftClient.program.account.user.coder.accounts.decode(
92
- 'User',
93
- buffer
94
- ) as UserAccount;
95
- this.tryUpdateUserAccount(key, userAccount, slot);
90
+ this.tryUpdateUserAccount(
91
+ key,
92
+ 'raw',
93
+ programAccount.account.data,
94
+ slot
95
+ );
96
96
  }
97
97
 
98
98
  for (const key of this.usersAccounts.keys()) {
@@ -110,11 +110,31 @@ export class OrderSubscriber {
110
110
 
111
111
  tryUpdateUserAccount(
112
112
  key: string,
113
- userAccount: UserAccount,
113
+ dataType: 'raw' | 'decoded',
114
+ data: string[] | UserAccount,
114
115
  slot: number
115
116
  ): void {
117
+ if (!this.mostRecentSlot || slot > this.mostRecentSlot) {
118
+ this.mostRecentSlot = slot;
119
+ }
120
+
116
121
  const slotAndUserAccount = this.usersAccounts.get(key);
117
- if (!slotAndUserAccount || slotAndUserAccount.slot < slot) {
122
+ if (!slotAndUserAccount || slotAndUserAccount.slot <= slot) {
123
+ let userAccount: UserAccount;
124
+ // Polling leads to a lot of redundant decoding, so we only decode if data is from a fresh slot
125
+ if (dataType === 'raw') {
126
+ // @ts-ignore
127
+ const buffer = Buffer.from(data[0], data[1]);
128
+
129
+ userAccount =
130
+ this.driftClient.program.account.user.coder.accounts.decodeUnchecked(
131
+ 'User',
132
+ buffer
133
+ ) as UserAccount;
134
+ } else {
135
+ userAccount = data as UserAccount;
136
+ }
137
+
118
138
  const newOrders = userAccount.orders.filter(
119
139
  (order) =>
120
140
  order.slot.toNumber() > (slotAndUserAccount?.slot ?? 0) &&
@@ -148,6 +168,10 @@ export class OrderSubscriber {
148
168
  return dlob;
149
169
  }
150
170
 
171
+ public getSlot(): number {
172
+ return this.mostRecentSlot ?? 0;
173
+ }
174
+
151
175
  public async unsubscribe(): Promise<void> {
152
176
  await this.subscription.unsubscribe();
153
177
  }
@@ -2,10 +2,11 @@ import { OrderSubscriber } from './OrderSubscriber';
2
2
  import { getNonIdleUserFilter, getUserFilter } from '../memcmp';
3
3
  import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber';
4
4
  import { UserAccount } from '../types';
5
- import { Context, PublicKey } from '@solana/web3.js';
5
+ import { Commitment, Context, PublicKey } from '@solana/web3.js';
6
6
 
7
7
  export class WebsocketSubscription {
8
8
  private orderSubscriber: OrderSubscriber;
9
+ private commitment: Commitment;
9
10
  private skipInitialLoad: boolean;
10
11
  private resubTimeoutMs?: number;
11
12
 
@@ -13,14 +14,17 @@ export class WebsocketSubscription {
13
14
 
14
15
  constructor({
15
16
  orderSubscriber,
17
+ commitment,
16
18
  skipInitialLoad = false,
17
19
  resubTimeoutMs,
18
20
  }: {
19
21
  orderSubscriber: OrderSubscriber;
22
+ commitment: Commitment;
20
23
  skipInitialLoad?: boolean;
21
24
  resubTimeoutMs?: number;
22
25
  }) {
23
26
  this.orderSubscriber = orderSubscriber;
27
+ this.commitment = commitment;
24
28
  this.skipInitialLoad = skipInitialLoad;
25
29
  this.resubTimeoutMs = resubTimeoutMs;
26
30
  }
@@ -36,7 +40,7 @@ export class WebsocketSubscription {
36
40
  ),
37
41
  {
38
42
  filters: [getUserFilter(), getNonIdleUserFilter()],
39
- commitment: this.orderSubscriber.driftClient.opts.commitment,
43
+ commitment: this.commitment,
40
44
  },
41
45
  this.resubTimeoutMs
42
46
  );
@@ -47,6 +51,7 @@ export class WebsocketSubscription {
47
51
  const userKey = accountId.toBase58();
48
52
  this.orderSubscriber.tryUpdateUserAccount(
49
53
  userKey,
54
+ 'decoded',
50
55
  account,
51
56
  context.slot
52
57
  );
@@ -1,4 +1,4 @@
1
- import { PublicKey } from '@solana/web3.js';
1
+ import { Commitment, PublicKey } from '@solana/web3.js';
2
2
  import { Order, UserAccount } from '../types';
3
3
  import { DriftClient } from '../driftClient';
4
4
 
@@ -8,11 +8,13 @@ export type OrderSubscriberConfig = {
8
8
  | {
9
9
  type: 'polling';
10
10
  frequency: number;
11
+ commitment?: Commitment;
11
12
  }
12
13
  | {
13
14
  type: 'websocket';
14
15
  skipInitialLoad?: boolean;
15
16
  resubTimeoutMs?: number;
17
+ commitment?: Commitment;
16
18
  };
17
19
  };
18
20
 
@@ -29,8 +29,10 @@ export class SlotSubscriber {
29
29
  this.currentSlot = await this.connection.getSlot('confirmed');
30
30
 
31
31
  this.subscriptionId = this.connection.onSlotChange((slotInfo) => {
32
- this.currentSlot = slotInfo.slot;
33
- this.eventEmitter.emit('newSlot', slotInfo.slot);
32
+ if (!this.currentSlot || this.currentSlot < slotInfo.slot) {
33
+ this.currentSlot = slotInfo.slot;
34
+ this.eventEmitter.emit('newSlot', slotInfo.slot);
35
+ }
34
36
  });
35
37
  }
36
38
 
package/src/user.ts CHANGED
@@ -109,12 +109,12 @@ export class User {
109
109
  );
110
110
  } else if (config.accountSubscription?.type === 'custom') {
111
111
  this.accountSubscriber = config.accountSubscription.userAccountSubscriber;
112
- } else if (config.accountSubscription?.type === 'websocket') {
112
+ } else {
113
113
  this.accountSubscriber = new WebSocketUserAccountSubscriber(
114
114
  config.driftClient.program,
115
115
  config.userAccountPublicKey,
116
- config.accountSubscription.resubTimeoutMs,
117
- config.accountSubscription.commitment
116
+ config.accountSubscription?.resubTimeoutMs,
117
+ config.accountSubscription?.commitment
118
118
  );
119
119
  }
120
120
  this.eventEmitter = this.accountSubscriber.eventEmitter;