@drift-labs/sdk 2.48.0-beta.1 → 2.48.0-beta.11

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 CHANGED
@@ -1 +1 @@
1
- 2.48.0-beta.1
1
+ 2.48.0-beta.11
@@ -53,12 +53,17 @@ class PollingUserAccountSubscriber {
53
53
  }
54
54
  async fetch() {
55
55
  var _a, _b;
56
- await this.accountLoader.load();
57
- const { buffer, slot } = this.accountLoader.getBufferAndSlot(this.userAccountPublicKey);
58
- const currentSlot = (_b = (_a = this.user) === null || _a === void 0 ? void 0 : _a.slot) !== null && _b !== void 0 ? _b : 0;
59
- if (buffer && slot > currentSlot) {
60
- const account = this.program.account.user.coder.accounts.decode('User', buffer);
61
- this.user = { data: account, slot };
56
+ try {
57
+ const dataAndContext = await this.program.account.user.fetchAndContext(this.userAccountPublicKey, this.accountLoader.commitment);
58
+ if (dataAndContext.context.slot > ((_b = (_a = this.user) === null || _a === void 0 ? void 0 : _a.slot) !== null && _b !== void 0 ? _b : 0)) {
59
+ this.user = {
60
+ data: dataAndContext.data,
61
+ slot: dataAndContext.context.slot,
62
+ };
63
+ }
64
+ }
65
+ catch (e) {
66
+ console.log(`PollingUserAccountSubscriber.fetch() UserAccount does not exist: ${e.message}`);
62
67
  }
63
68
  }
64
69
  doesAccountExist() {
@@ -80,7 +85,9 @@ class PollingUserAccountSubscriber {
80
85
  }
81
86
  }
82
87
  getUserAccountAndSlot() {
83
- this.assertIsSubscribed();
88
+ if (!this.doesAccountExist()) {
89
+ throw new types_1.NotSubscribedError('You must call `subscribe` or `fetch` before using this function');
90
+ }
84
91
  return this.user;
85
92
  }
86
93
  updateData(userAccount, slot) {
@@ -53,12 +53,17 @@ class PollingUserStatsAccountSubscriber {
53
53
  }
54
54
  async fetch() {
55
55
  var _a, _b;
56
- await this.accountLoader.load();
57
- const { buffer, slot } = this.accountLoader.getBufferAndSlot(this.userStatsAccountPublicKey);
58
- const currentSlot = (_b = (_a = this.userStats) === null || _a === void 0 ? void 0 : _a.slot) !== null && _b !== void 0 ? _b : 0;
59
- if (buffer && slot > currentSlot) {
60
- const account = this.program.account.userStats.coder.accounts.decodeUnchecked('UserStats', buffer);
61
- this.userStats = { data: account, slot };
56
+ try {
57
+ const dataAndContext = await this.program.account.userStats.fetchAndContext(this.userStatsAccountPublicKey, this.accountLoader.commitment);
58
+ if (dataAndContext.context.slot > ((_b = (_a = this.userStats) === null || _a === void 0 ? void 0 : _a.slot) !== null && _b !== void 0 ? _b : 0)) {
59
+ this.userStats = {
60
+ data: dataAndContext.data,
61
+ slot: dataAndContext.context.slot,
62
+ };
63
+ }
64
+ }
65
+ catch (e) {
66
+ console.log(`PollingUserStatsAccountSubscriber.fetch() UserStatsAccount does not exist: ${e.message}`);
62
67
  }
63
68
  }
64
69
  doesAccountExist() {
@@ -80,7 +85,9 @@ class PollingUserStatsAccountSubscriber {
80
85
  }
81
86
  }
82
87
  getUserStatsAccountAndSlot() {
83
- this.assertIsSubscribed();
88
+ if (!this.doesAccountExist()) {
89
+ throw new types_1.NotSubscribedError('You must call `subscribe` or `fetch` before using this function');
90
+ }
84
91
  return this.userStats;
85
92
  }
86
93
  }
@@ -93,7 +93,7 @@ function getVammL2Generator({ marketAccount, oraclePriceData, numOrders, now, to
93
93
  }
94
94
  const updatedAmm = (0, __1.calculateUpdatedAMM)(marketAccount.amm, oraclePriceData);
95
95
  let [openBids, openAsks] = (0, __1.calculateMarketOpenBidAsk)(updatedAmm.baseAssetReserve, updatedAmm.minBaseAssetReserve, updatedAmm.maxBaseAssetReserve, updatedAmm.orderStepSize);
96
- const minOrderSize = marketAccount.amm.orderStepSize;
96
+ const minOrderSize = marketAccount.amm.minOrderSize;
97
97
  if (openBids.lt(minOrderSize.muln(2))) {
98
98
  openBids = __1.ZERO;
99
99
  }
@@ -1150,7 +1150,7 @@ class DriftClient {
1150
1150
  withdrawIxs.push((0, spl_token_1.createCloseAccountInstruction)(associatedTokenAddress, authority, authority, []));
1151
1151
  }
1152
1152
  const tx = await this.buildTransaction(withdrawIxs, {
1153
- computeUnits: 600000,
1153
+ computeUnits: 1400000,
1154
1154
  });
1155
1155
  const { txSig, slot } = await this.sendTransaction(tx, additionalSigners, this.opts);
1156
1156
  this.spotMarketLastSlotCache.set(marketIndex, slot);
@@ -2647,9 +2647,9 @@ class DriftClient {
2647
2647
  auctionDuration: auctionDuration || 0,
2648
2648
  auctionStartPrice: auctionStartPrice || numericConstants_1.ZERO,
2649
2649
  auctionEndPrice: auctionEndPrice || numericConstants_1.ZERO,
2650
- reduceOnly: reduceOnly || false,
2651
- postOnly: postOnly || null,
2652
- immediateOrCancel: immediateOrCancel || false,
2650
+ reduceOnly: reduceOnly != undefined ? reduceOnly : null,
2651
+ postOnly: postOnly != undefined ? postOnly : null,
2652
+ immediateOrCancel: immediateOrCancel != undefined ? immediateOrCancel : null,
2653
2653
  policy: policy || null,
2654
2654
  maxTs: maxTs || null,
2655
2655
  };
@@ -2698,9 +2698,9 @@ class DriftClient {
2698
2698
  oraclePriceOffset: newOraclePriceOffset || null,
2699
2699
  triggerPrice: newTriggerPrice || null,
2700
2700
  triggerCondition: newTriggerCondition || null,
2701
- auctionDuration: auctionDuration || 0,
2702
- auctionStartPrice: auctionStartPrice || numericConstants_1.ZERO,
2703
- auctionEndPrice: auctionEndPrice || numericConstants_1.ZERO,
2701
+ auctionDuration: auctionDuration || null,
2702
+ auctionStartPrice: auctionStartPrice || null,
2703
+ auctionEndPrice: auctionEndPrice || null,
2704
2704
  reduceOnly: reduceOnly || false,
2705
2705
  postOnly: postOnly || null,
2706
2706
  immediateOrCancel: immediateOrCancel || false,
@@ -17,10 +17,12 @@ export declare class OrderSubscriber {
17
17
  eventEmitter: StrictEventEmitter<EventEmitter, OrderSubscriberEvents>;
18
18
  fetchPromise?: Promise<void>;
19
19
  fetchPromiseResolver: () => void;
20
+ mostRecentSlot: number;
20
21
  constructor(config: OrderSubscriberConfig);
21
22
  subscribe(): Promise<void>;
22
23
  fetch(): Promise<void>;
23
24
  tryUpdateUserAccount(key: string, userAccount: UserAccount, slot: number): void;
24
25
  getDLOB(slot: number): Promise<DLOB>;
26
+ getSlot(): number;
25
27
  unsubscribe(): Promise<void>;
26
28
  }
@@ -76,6 +76,9 @@ class OrderSubscriber {
76
76
  }
77
77
  }
78
78
  tryUpdateUserAccount(key, userAccount, slot) {
79
+ if (!this.mostRecentSlot || slot > this.mostRecentSlot) {
80
+ this.mostRecentSlot = slot;
81
+ }
79
82
  const slotAndUserAccount = this.usersAccounts.get(key);
80
83
  if (!slotAndUserAccount || slotAndUserAccount.slot < slot) {
81
84
  const newOrders = userAccount.orders.filter((order) => {
@@ -104,6 +107,10 @@ class OrderSubscriber {
104
107
  }
105
108
  return dlob;
106
109
  }
110
+ getSlot() {
111
+ var _a;
112
+ return (_a = this.mostRecentSlot) !== null && _a !== void 0 ? _a : 0;
113
+ }
107
114
  async unsubscribe() {
108
115
  await this.subscription.unsubscribe();
109
116
  }
package/lib/user.js CHANGED
@@ -22,7 +22,7 @@ class User {
22
22
  this._isSubscribed = val;
23
23
  }
24
24
  constructor(config) {
25
- var _a, _b, _c;
25
+ var _a, _b;
26
26
  this._isSubscribed = false;
27
27
  this.driftClient = config.driftClient;
28
28
  this.userAccountPublicKey = config.userAccountPublicKey;
@@ -32,7 +32,7 @@ class User {
32
32
  else if (((_b = config.accountSubscription) === null || _b === void 0 ? void 0 : _b.type) === 'custom') {
33
33
  this.accountSubscriber = config.accountSubscription.userAccountSubscriber;
34
34
  }
35
- else if (((_c = config.accountSubscription) === null || _c === void 0 ? void 0 : _c.type) === 'websocket') {
35
+ else {
36
36
  this.accountSubscriber = new webSocketUserAccountSubscriber_1.WebSocketUserAccountSubscriber(config.driftClient.program, config.userAccountPublicKey, config.accountSubscription.resubTimeoutMs, config.accountSubscription.commitment);
37
37
  }
38
38
  this.eventEmitter = this.accountSubscriber.eventEmitter;
@@ -11,20 +11,31 @@ export interface UserMapInterface {
11
11
  updateWithOrderRecord(record: OrderRecord): Promise<void>;
12
12
  values(): IterableIterator<User>;
13
13
  }
14
+ export type SyncCallbackCriteria = {
15
+ hasOpenOrders: boolean;
16
+ };
14
17
  export declare class UserMap implements UserMapInterface {
15
18
  private userMap;
16
19
  private driftClient;
17
20
  private accountSubscription;
18
21
  private includeIdle;
19
22
  private lastNumberOfSubAccounts;
23
+ private stateAccountUpdateCallback;
20
24
  private syncCallback;
25
+ private syncCallbackCriteria;
26
+ private syncPromise?;
27
+ private syncPromiseResolver;
21
28
  /**
29
+ * Constructs a new UserMap instance.
22
30
  *
23
- * @param driftClient
24
- * @param accountSubscription
25
- * @param includeIdle whether idle users are subscribed to. defaults to false to decrease # of user subscriptions
31
+ * @param {DriftClient} driftClient - The DriftClient instance.
32
+ * @param {UserSubscriptionConfig} accountSubscription - The UserSubscriptionConfig instance.
33
+ * @param {boolean} includeIdle - Whether idle users are subscribed to. Defaults to false to decrease # of user subscriptions.
34
+ * @param {(authorities: PublicKey[]) => Promise<void>} syncCallback - Called after `sync` completes, will pas in unique list of authorities. Useful for using it to sync UserStatsMap.
35
+ * @param {SyncCallbackCriteria} syncCallbackCriteria - The criteria for the sync callback. Defaults to having no filters
26
36
  */
27
- constructor(driftClient: DriftClient, accountSubscription: UserSubscriptionConfig, includeIdle?: boolean);
37
+ constructor(driftClient: DriftClient, accountSubscription: UserSubscriptionConfig, includeIdle?: boolean, syncCallback?: (authorities: PublicKey[]) => Promise<void>, syncCallbackCriteria?: SyncCallbackCriteria);
38
+ addSyncCallback(syncCallback?: (authorities: PublicKey[]) => Promise<void>, syncCallbackCriteria?: SyncCallbackCriteria): void;
28
39
  subscribe(): Promise<void>;
29
40
  addPubkey(userAccountPublicKey: PublicKey, userAccount?: UserAccount): Promise<void>;
30
41
  has(key: string): boolean;
@@ -56,6 +67,7 @@ export declare class UserMap implements UserMapInterface {
56
67
  updateWithEventRecord(record: WrappedEvent<any>): Promise<void>;
57
68
  values(): IterableIterator<User>;
58
69
  size(): number;
70
+ getUniqueAuthorities(useSyncCallbackCriteria?: boolean): PublicKey[];
59
71
  sync(): Promise<void>;
60
72
  unsubscribe(): Promise<void>;
61
73
  }
@@ -7,14 +7,17 @@ const buffer_1 = require("buffer");
7
7
  const memcmp_1 = require("../memcmp");
8
8
  class UserMap {
9
9
  /**
10
+ * Constructs a new UserMap instance.
10
11
  *
11
- * @param driftClient
12
- * @param accountSubscription
13
- * @param includeIdle whether idle users are subscribed to. defaults to false to decrease # of user subscriptions
12
+ * @param {DriftClient} driftClient - The DriftClient instance.
13
+ * @param {UserSubscriptionConfig} accountSubscription - The UserSubscriptionConfig instance.
14
+ * @param {boolean} includeIdle - Whether idle users are subscribed to. Defaults to false to decrease # of user subscriptions.
15
+ * @param {(authorities: PublicKey[]) => Promise<void>} syncCallback - Called after `sync` completes, will pas in unique list of authorities. Useful for using it to sync UserStatsMap.
16
+ * @param {SyncCallbackCriteria} syncCallbackCriteria - The criteria for the sync callback. Defaults to having no filters
14
17
  */
15
- constructor(driftClient, accountSubscription, includeIdle = false) {
18
+ constructor(driftClient, accountSubscription, includeIdle = false, syncCallback, syncCallbackCriteria = { hasOpenOrders: false }) {
16
19
  this.userMap = new Map();
17
- this.syncCallback = async (state) => {
20
+ this.stateAccountUpdateCallback = async (state) => {
18
21
  if (state.numberOfSubAccounts !== this.lastNumberOfSubAccounts) {
19
22
  await this.sync();
20
23
  this.lastNumberOfSubAccounts = state.numberOfSubAccounts;
@@ -23,6 +26,12 @@ class UserMap {
23
26
  this.driftClient = driftClient;
24
27
  this.accountSubscription = accountSubscription;
25
28
  this.includeIdle = includeIdle;
29
+ this.syncCallback = syncCallback;
30
+ this.syncCallbackCriteria = syncCallbackCriteria;
31
+ }
32
+ addSyncCallback(syncCallback, syncCallbackCriteria = { hasOpenOrders: false }) {
33
+ this.syncCallback = syncCallback;
34
+ this.syncCallbackCriteria = syncCallbackCriteria;
26
35
  }
27
36
  async subscribe() {
28
37
  if (this.size() > 0) {
@@ -31,7 +40,7 @@ class UserMap {
31
40
  await this.driftClient.subscribe();
32
41
  this.lastNumberOfSubAccounts =
33
42
  this.driftClient.getStateAccount().numberOfSubAccounts;
34
- this.driftClient.eventEmitter.on('stateAccountUpdate', this.syncCallback);
43
+ this.driftClient.eventEmitter.on('stateAccountUpdate', this.stateAccountUpdateCallback);
35
44
  await this.sync();
36
45
  }
37
46
  async addPubkey(userAccountPublicKey, userAccount) {
@@ -139,45 +148,78 @@ class UserMap {
139
148
  size() {
140
149
  return this.userMap.size;
141
150
  }
151
+ getUniqueAuthorities(useSyncCallbackCriteria = true) {
152
+ const usersMeetingCriteria = Array.from(this.userMap.values()).filter((user) => {
153
+ let pass = true;
154
+ if (useSyncCallbackCriteria &&
155
+ this.syncCallbackCriteria.hasOpenOrders) {
156
+ pass = pass && user.getUserAccount().hasOpenOrder;
157
+ }
158
+ return pass;
159
+ });
160
+ const userAuths = new Set(usersMeetingCriteria.map((user) => user.getUserAccount().authority.toBase58()));
161
+ const userAuthKeys = Array.from(userAuths).map((userAuth) => new web3_js_1.PublicKey(userAuth));
162
+ return userAuthKeys;
163
+ }
142
164
  async sync() {
143
- const filters = [(0, memcmp_1.getUserFilter)()];
144
- if (!this.includeIdle) {
145
- filters.push((0, memcmp_1.getNonIdleUserFilter)());
146
- }
147
- const rpcRequestArgs = [
148
- this.driftClient.program.programId.toBase58(),
149
- {
150
- commitment: this.driftClient.connection.commitment,
151
- filters,
152
- encoding: 'base64',
153
- withContext: true,
154
- },
155
- ];
156
- // @ts-ignore
157
- const rpcJSONResponse = await this.driftClient.connection._rpcRequest('getProgramAccounts', rpcRequestArgs);
158
- const rpcResponseAndContext = rpcJSONResponse.result;
159
- const slot = rpcResponseAndContext.context.slot;
160
- const programAccountBufferMap = new Map();
161
- for (const programAccount of rpcResponseAndContext.value) {
162
- programAccountBufferMap.set(programAccount.pubkey.toString(),
163
- // @ts-ignore
164
- buffer_1.Buffer.from(programAccount.account.data[0], programAccount.account.data[1]));
165
+ if (this.syncPromise) {
166
+ return this.syncPromise;
165
167
  }
166
- for (const [key, buffer] of programAccountBufferMap.entries()) {
167
- if (!this.has(key)) {
168
- const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', buffer);
169
- await this.addPubkey(new web3_js_1.PublicKey(key), userAccount);
168
+ this.syncPromise = new Promise((resolver) => {
169
+ this.syncPromiseResolver = resolver;
170
+ });
171
+ try {
172
+ const filters = [(0, memcmp_1.getUserFilter)()];
173
+ if (!this.includeIdle) {
174
+ filters.push((0, memcmp_1.getNonIdleUserFilter)());
170
175
  }
171
- }
172
- for (const [key, user] of this.userMap.entries()) {
173
- if (!programAccountBufferMap.has(key)) {
174
- await user.unsubscribe();
175
- this.userMap.delete(key);
176
+ const rpcRequestArgs = [
177
+ this.driftClient.program.programId.toBase58(),
178
+ {
179
+ commitment: this.driftClient.connection.commitment,
180
+ filters,
181
+ encoding: 'base64',
182
+ withContext: true,
183
+ },
184
+ ];
185
+ const rpcJSONResponse =
186
+ // @ts-ignore
187
+ await this.driftClient.connection._rpcRequest('getProgramAccounts', rpcRequestArgs);
188
+ const rpcResponseAndContext = rpcJSONResponse.result;
189
+ const slot = rpcResponseAndContext.context.slot;
190
+ const programAccountBufferMap = new Map();
191
+ for (const programAccount of rpcResponseAndContext.value) {
192
+ programAccountBufferMap.set(programAccount.pubkey.toString(),
193
+ // @ts-ignore
194
+ buffer_1.Buffer.from(programAccount.account.data[0], programAccount.account.data[1]));
195
+ }
196
+ for (const [key, buffer] of programAccountBufferMap.entries()) {
197
+ if (!this.has(key)) {
198
+ const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', buffer);
199
+ await this.addPubkey(new web3_js_1.PublicKey(key), userAccount);
200
+ }
176
201
  }
177
- else {
178
- const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', programAccountBufferMap.get(key));
179
- user.accountSubscriber.updateData(userAccount, slot);
202
+ for (const [key, user] of this.userMap.entries()) {
203
+ if (!programAccountBufferMap.has(key)) {
204
+ await user.unsubscribe();
205
+ this.userMap.delete(key);
206
+ }
207
+ else {
208
+ const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', programAccountBufferMap.get(key));
209
+ user.accountSubscriber.updateData(userAccount, slot);
210
+ }
180
211
  }
212
+ if (this.syncCallback) {
213
+ await this.syncCallback(this.getUniqueAuthorities());
214
+ }
215
+ }
216
+ catch (e) {
217
+ console.error(`Error in UserMap.sync()`);
218
+ console.error(e);
219
+ }
220
+ finally {
221
+ this.syncPromiseResolver();
222
+ this.syncPromise = undefined;
181
223
  }
182
224
  }
183
225
  async unsubscribe() {
@@ -186,7 +228,7 @@ class UserMap {
186
228
  this.userMap.delete(key);
187
229
  }
188
230
  if (this.lastNumberOfSubAccounts) {
189
- this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.syncCallback);
231
+ this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.stateAccountUpdateCallback);
190
232
  this.lastNumberOfSubAccounts = undefined;
191
233
  }
192
234
  }
@@ -1,4 +1,4 @@
1
- import { DriftClient, OrderRecord, UserStatsAccount, UserStats, UserStatsSubscriptionConfig, WrappedEvent } from '..';
1
+ import { DriftClient, OrderRecord, UserStatsAccount, UserStats, WrappedEvent, BulkAccountLoader } from '..';
2
2
  import { PublicKey } from '@solana/web3.js';
3
3
  import { UserMap } from './userMap';
4
4
  export declare class UserStatsMap {
@@ -7,19 +7,40 @@ export declare class UserStatsMap {
7
7
  */
8
8
  private userStatsMap;
9
9
  private driftClient;
10
- private accountSubscription;
11
- private lastNumberOfAuthorities;
12
- private syncCallback;
13
- constructor(driftClient: DriftClient, accountSubscription: UserStatsSubscriptionConfig);
14
- subscribe(): Promise<void>;
15
- addUserStat(authority: PublicKey, userStatsAccount?: UserStatsAccount): Promise<void>;
10
+ private bulkAccountLoader;
11
+ /**
12
+ * Creates a new UserStatsMap instance.
13
+ *
14
+ * @param {DriftClient} driftClient - The DriftClient instance.
15
+ * @param {BulkAccountLoader} [bulkAccountLoader] - If not provided, a new BulkAccountLoader with polling disabled will be created.
16
+ */
17
+ constructor(driftClient: DriftClient, bulkAccountLoader?: BulkAccountLoader);
18
+ subscribe(authorities: PublicKey[]): Promise<void>;
19
+ /**
20
+ *
21
+ * @param authority that owns the UserStatsAccount
22
+ * @param userStatsAccount optional UserStatsAccount to subscribe to, if undefined will be fetched later
23
+ * @param skipFetch if true, will not immediately fetch the UserStatsAccount
24
+ */
25
+ addUserStat(authority: PublicKey, userStatsAccount?: UserStatsAccount, skipFetch?: boolean): Promise<void>;
16
26
  updateWithOrderRecord(record: OrderRecord, userMap: UserMap): Promise<void>;
17
27
  updateWithEventRecord(record: WrappedEvent<any>, userMap?: UserMap): Promise<void>;
18
28
  has(authorityPublicKey: string): boolean;
19
29
  get(authorityPublicKey: string): UserStats;
30
+ /**
31
+ * Enforce that a UserStats will exist for the given authorityPublicKey,
32
+ * reading one from the blockchain if necessary.
33
+ * @param authorityPublicKey
34
+ * @returns
35
+ */
20
36
  mustGet(authorityPublicKey: string): Promise<UserStats>;
21
37
  values(): IterableIterator<UserStats>;
22
38
  size(): number;
23
- sync(): Promise<void>;
39
+ /**
40
+ * Sync the UserStatsMap
41
+ * @param authorities list of authorities to derive UserStatsAccount public keys from.
42
+ * You may want to get this list from UserMap in order to filter out idle users
43
+ */
44
+ sync(authorities: PublicKey[]): Promise<void>;
24
45
  unsubscribe(): Promise<void>;
25
46
  }
@@ -4,43 +4,57 @@ exports.UserStatsMap = void 0;
4
4
  const __1 = require("..");
5
5
  const web3_js_1 = require("@solana/web3.js");
6
6
  class UserStatsMap {
7
- constructor(driftClient, accountSubscription) {
7
+ /**
8
+ * Creates a new UserStatsMap instance.
9
+ *
10
+ * @param {DriftClient} driftClient - The DriftClient instance.
11
+ * @param {BulkAccountLoader} [bulkAccountLoader] - If not provided, a new BulkAccountLoader with polling disabled will be created.
12
+ */
13
+ constructor(driftClient, bulkAccountLoader) {
8
14
  /**
9
15
  * map from authority pubkey to UserStats
10
16
  */
11
17
  this.userStatsMap = new Map();
12
- this.syncCallback = async (state) => {
13
- if (state.numberOfAuthorities !== this.lastNumberOfAuthorities) {
14
- await this.sync();
15
- this.lastNumberOfAuthorities = state.numberOfAuthorities;
16
- }
17
- };
18
18
  this.driftClient = driftClient;
19
- this.accountSubscription = accountSubscription;
19
+ if (!bulkAccountLoader) {
20
+ bulkAccountLoader = new __1.BulkAccountLoader(driftClient.connection, driftClient.opts.commitment, 0);
21
+ }
22
+ this.bulkAccountLoader = bulkAccountLoader;
20
23
  }
21
- async subscribe() {
24
+ async subscribe(authorities) {
22
25
  if (this.size() > 0) {
23
26
  return;
24
27
  }
25
28
  await this.driftClient.subscribe();
26
- this.lastNumberOfAuthorities =
27
- this.driftClient.getStateAccount().numberOfAuthorities;
28
- this.driftClient.eventEmitter.on('stateAccountUpdate', this.syncCallback);
29
- await this.sync();
29
+ await this.sync(authorities);
30
30
  }
31
- async addUserStat(authority, userStatsAccount) {
31
+ /**
32
+ *
33
+ * @param authority that owns the UserStatsAccount
34
+ * @param userStatsAccount optional UserStatsAccount to subscribe to, if undefined will be fetched later
35
+ * @param skipFetch if true, will not immediately fetch the UserStatsAccount
36
+ */
37
+ async addUserStat(authority, userStatsAccount, skipFetch) {
32
38
  const userStat = new __1.UserStats({
33
39
  driftClient: this.driftClient,
34
40
  userStatsAccountPublicKey: (0, __1.getUserStatsAccountPublicKey)(this.driftClient.program.programId, authority),
35
- accountSubscription: this.accountSubscription,
41
+ accountSubscription: {
42
+ type: 'polling',
43
+ accountLoader: this.bulkAccountLoader,
44
+ },
36
45
  });
37
- await userStat.subscribe(userStatsAccount);
46
+ if (skipFetch) {
47
+ await userStat.accountSubscriber.addToAccountLoader();
48
+ }
49
+ else {
50
+ await userStat.subscribe(userStatsAccount);
51
+ }
38
52
  this.userStatsMap.set(authority.toString(), userStat);
39
53
  }
40
54
  async updateWithOrderRecord(record, userMap) {
41
55
  const user = await userMap.mustGet(record.user.toString());
42
56
  if (!this.has(user.getUserAccount().authority.toString())) {
43
- await this.addUserStat(user.getUserAccount().authority);
57
+ await this.addUserStat(user.getUserAccount().authority, undefined, false);
44
58
  }
45
59
  }
46
60
  async updateWithEventRecord(record, userMap) {
@@ -114,9 +128,15 @@ class UserStatsMap {
114
128
  get(authorityPublicKey) {
115
129
  return this.userStatsMap.get(authorityPublicKey);
116
130
  }
131
+ /**
132
+ * Enforce that a UserStats will exist for the given authorityPublicKey,
133
+ * reading one from the blockchain if necessary.
134
+ * @param authorityPublicKey
135
+ * @returns
136
+ */
117
137
  async mustGet(authorityPublicKey) {
118
138
  if (!this.has(authorityPublicKey)) {
119
- await this.addUserStat(new web3_js_1.PublicKey(authorityPublicKey));
139
+ await this.addUserStat(new web3_js_1.PublicKey(authorityPublicKey), undefined, false);
120
140
  }
121
141
  return this.get(authorityPublicKey);
122
142
  }
@@ -126,35 +146,20 @@ class UserStatsMap {
126
146
  size() {
127
147
  return this.userStatsMap.size;
128
148
  }
129
- async sync() {
130
- const programAccounts = await this.driftClient.connection.getProgramAccounts(this.driftClient.program.programId, {
131
- commitment: this.driftClient.connection.commitment,
132
- filters: [
133
- {
134
- memcmp: this.driftClient.program.coder.accounts.memcmp('UserStats'),
135
- },
136
- ],
137
- });
138
- const programAccountMap = new Map();
139
- for (const programAccount of programAccounts) {
140
- programAccountMap.set(new web3_js_1.PublicKey(programAccount.account.data.slice(8, 40)).toString(), programAccount.account);
141
- }
142
- for (const key of programAccountMap.keys()) {
143
- if (!this.has(key)) {
144
- const userStatsAccount = this.driftClient.program.account.userStats.coder.accounts.decode('UserStats', programAccountMap.get(key).data);
145
- await this.addUserStat(new web3_js_1.PublicKey(key), userStatsAccount);
146
- }
147
- }
149
+ /**
150
+ * Sync the UserStatsMap
151
+ * @param authorities list of authorities to derive UserStatsAccount public keys from.
152
+ * You may want to get this list from UserMap in order to filter out idle users
153
+ */
154
+ async sync(authorities) {
155
+ await Promise.all(authorities.map((authority) => this.addUserStat(authority, undefined, true)));
156
+ await this.bulkAccountLoader.load();
148
157
  }
149
158
  async unsubscribe() {
150
159
  for (const [key, userStats] of this.userStatsMap.entries()) {
151
160
  await userStats.unsubscribe();
152
161
  this.userStatsMap.delete(key);
153
162
  }
154
- if (this.lastNumberOfAuthorities) {
155
- this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.syncCallback);
156
- this.lastNumberOfAuthorities = undefined;
157
- }
158
163
  }
159
164
  }
160
165
  exports.UserStatsMap = UserStatsMap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drift-labs/sdk",
3
- "version": "2.48.0-beta.1",
3
+ "version": "2.48.0-beta.11",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "author": "crispheaney",
@@ -93,17 +93,21 @@ export class PollingUserAccountSubscriber implements UserAccountSubscriber {
93
93
  }
94
94
 
95
95
  async fetch(): Promise<void> {
96
- await this.accountLoader.load();
97
- const { buffer, slot } = this.accountLoader.getBufferAndSlot(
98
- this.userAccountPublicKey
99
- );
100
- const currentSlot = this.user?.slot ?? 0;
101
- if (buffer && slot > currentSlot) {
102
- const account = this.program.account.user.coder.accounts.decode(
103
- 'User',
104
- buffer
96
+ try {
97
+ const dataAndContext = await this.program.account.user.fetchAndContext(
98
+ this.userAccountPublicKey,
99
+ this.accountLoader.commitment
100
+ );
101
+ if (dataAndContext.context.slot > (this.user?.slot ?? 0)) {
102
+ this.user = {
103
+ data: dataAndContext.data as UserAccount,
104
+ slot: dataAndContext.context.slot,
105
+ };
106
+ }
107
+ } catch (e) {
108
+ console.log(
109
+ `PollingUserAccountSubscriber.fetch() UserAccount does not exist: ${e.message}`
105
110
  );
106
- this.user = { data: account, slot };
107
111
  }
108
112
  }
109
113
 
@@ -137,7 +141,11 @@ export class PollingUserAccountSubscriber implements UserAccountSubscriber {
137
141
  }
138
142
 
139
143
  public getUserAccountAndSlot(): DataAndSlot<UserAccount> {
140
- this.assertIsSubscribed();
144
+ if (!this.doesAccountExist()) {
145
+ throw new NotSubscribedError(
146
+ 'You must call `subscribe` or `fetch` before using this function'
147
+ );
148
+ }
141
149
  return this.user;
142
150
  }
143
151
 
@@ -97,18 +97,22 @@ export class PollingUserStatsAccountSubscriber
97
97
  }
98
98
 
99
99
  async fetch(): Promise<void> {
100
- await this.accountLoader.load();
101
- const { buffer, slot } = this.accountLoader.getBufferAndSlot(
102
- this.userStatsAccountPublicKey
103
- );
104
- const currentSlot = this.userStats?.slot ?? 0;
105
- if (buffer && slot > currentSlot) {
106
- const account =
107
- this.program.account.userStats.coder.accounts.decodeUnchecked(
108
- 'UserStats',
109
- buffer
100
+ try {
101
+ const dataAndContext =
102
+ await this.program.account.userStats.fetchAndContext(
103
+ this.userStatsAccountPublicKey,
104
+ this.accountLoader.commitment
110
105
  );
111
- this.userStats = { data: account, slot };
106
+ if (dataAndContext.context.slot > (this.userStats?.slot ?? 0)) {
107
+ this.userStats = {
108
+ data: dataAndContext.data as UserStatsAccount,
109
+ slot: dataAndContext.context.slot,
110
+ };
111
+ }
112
+ } catch (e) {
113
+ console.log(
114
+ `PollingUserStatsAccountSubscriber.fetch() UserStatsAccount does not exist: ${e.message}`
115
+ );
112
116
  }
113
117
  }
114
118
 
@@ -142,7 +146,11 @@ export class PollingUserStatsAccountSubscriber
142
146
  }
143
147
 
144
148
  public getUserStatsAccountAndSlot(): DataAndSlot<UserStatsAccount> {
145
- this.assertIsSubscribed();
149
+ if (!this.doesAccountExist()) {
150
+ throw new NotSubscribedError(
151
+ 'You must call `subscribe` or `fetch` before using this function'
152
+ );
153
+ }
146
154
  return this.userStats;
147
155
  }
148
156
  }
@@ -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
  }
@@ -2028,7 +2028,7 @@ export class DriftClient {
2028
2028
  }
2029
2029
 
2030
2030
  const tx = await this.buildTransaction(withdrawIxs, {
2031
- computeUnits: 600_000,
2031
+ computeUnits: 1_400_000,
2032
2032
  });
2033
2033
  const { txSig, slot } = await this.sendTransaction(
2034
2034
  tx,
@@ -4654,9 +4654,10 @@ export class DriftClient {
4654
4654
  auctionDuration: auctionDuration || 0,
4655
4655
  auctionStartPrice: auctionStartPrice || ZERO,
4656
4656
  auctionEndPrice: auctionEndPrice || ZERO,
4657
- reduceOnly: reduceOnly || false,
4658
- postOnly: postOnly || null,
4659
- immediateOrCancel: immediateOrCancel || false,
4657
+ reduceOnly: reduceOnly != undefined ? reduceOnly : null,
4658
+ postOnly: postOnly != undefined ? postOnly : null,
4659
+ immediateOrCancel:
4660
+ immediateOrCancel != undefined ? immediateOrCancel : null,
4660
4661
  policy: policy || null,
4661
4662
  maxTs: maxTs || null,
4662
4663
  };
@@ -4769,9 +4770,9 @@ export class DriftClient {
4769
4770
  oraclePriceOffset: newOraclePriceOffset || null,
4770
4771
  triggerPrice: newTriggerPrice || null,
4771
4772
  triggerCondition: newTriggerCondition || null,
4772
- auctionDuration: auctionDuration || 0,
4773
- auctionStartPrice: auctionStartPrice || ZERO,
4774
- auctionEndPrice: auctionEndPrice || ZERO,
4773
+ auctionDuration: auctionDuration || null,
4774
+ auctionStartPrice: auctionStartPrice || null,
4775
+ auctionEndPrice: auctionEndPrice || null,
4775
4776
  reduceOnly: reduceOnly || false,
4776
4777
  postOnly: postOnly || null,
4777
4778
  immediateOrCancel: immediateOrCancel || false,
@@ -19,6 +19,8 @@ export class OrderSubscriber {
19
19
  fetchPromise?: Promise<void>;
20
20
  fetchPromiseResolver: () => void;
21
21
 
22
+ mostRecentSlot: number;
23
+
22
24
  constructor(config: OrderSubscriberConfig) {
23
25
  this.driftClient = config.driftClient;
24
26
  if (config.subscriptionConfig.type === 'polling') {
@@ -113,6 +115,10 @@ export class OrderSubscriber {
113
115
  userAccount: UserAccount,
114
116
  slot: number
115
117
  ): void {
118
+ if (!this.mostRecentSlot || slot > this.mostRecentSlot) {
119
+ this.mostRecentSlot = slot;
120
+ }
121
+
116
122
  const slotAndUserAccount = this.usersAccounts.get(key);
117
123
  if (!slotAndUserAccount || slotAndUserAccount.slot < slot) {
118
124
  const newOrders = userAccount.orders.filter(
@@ -148,6 +154,10 @@ export class OrderSubscriber {
148
154
  return dlob;
149
155
  }
150
156
 
157
+ public getSlot(): number {
158
+ return this.mostRecentSlot ?? 0;
159
+ }
160
+
151
161
  public async unsubscribe(): Promise<void> {
152
162
  await this.subscription.unsubscribe();
153
163
  }
package/src/user.ts CHANGED
@@ -109,7 +109,7 @@ 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,
@@ -32,33 +32,59 @@ export interface UserMapInterface {
32
32
  values(): IterableIterator<User>;
33
33
  }
34
34
 
35
+ // filter users that meet these criteria when passing into syncCallback
36
+ export type SyncCallbackCriteria = {
37
+ // only sync users that have open orders
38
+ hasOpenOrders: boolean;
39
+ };
40
+
35
41
  export class UserMap implements UserMapInterface {
36
42
  private userMap = new Map<string, User>();
37
43
  private driftClient: DriftClient;
38
44
  private accountSubscription: UserSubscriptionConfig;
39
45
  private includeIdle: boolean;
40
46
  private lastNumberOfSubAccounts;
41
- private syncCallback = async (state: StateAccount) => {
47
+ private stateAccountUpdateCallback = async (state: StateAccount) => {
42
48
  if (state.numberOfSubAccounts !== this.lastNumberOfSubAccounts) {
43
49
  await this.sync();
44
50
  this.lastNumberOfSubAccounts = state.numberOfSubAccounts;
45
51
  }
46
52
  };
53
+ private syncCallback: (authorities: PublicKey[]) => Promise<void>;
54
+ private syncCallbackCriteria: SyncCallbackCriteria;
55
+
56
+ private syncPromise?: Promise<void>;
57
+ private syncPromiseResolver: () => void;
47
58
 
48
59
  /**
60
+ * Constructs a new UserMap instance.
49
61
  *
50
- * @param driftClient
51
- * @param accountSubscription
52
- * @param includeIdle whether idle users are subscribed to. defaults to false to decrease # of user subscriptions
62
+ * @param {DriftClient} driftClient - The DriftClient instance.
63
+ * @param {UserSubscriptionConfig} accountSubscription - The UserSubscriptionConfig instance.
64
+ * @param {boolean} includeIdle - Whether idle users are subscribed to. Defaults to false to decrease # of user subscriptions.
65
+ * @param {(authorities: PublicKey[]) => Promise<void>} syncCallback - Called after `sync` completes, will pas in unique list of authorities. Useful for using it to sync UserStatsMap.
66
+ * @param {SyncCallbackCriteria} syncCallbackCriteria - The criteria for the sync callback. Defaults to having no filters
53
67
  */
54
68
  constructor(
55
69
  driftClient: DriftClient,
56
70
  accountSubscription: UserSubscriptionConfig,
57
- includeIdle = false
71
+ includeIdle = false,
72
+ syncCallback?: (authorities: PublicKey[]) => Promise<void>,
73
+ syncCallbackCriteria: SyncCallbackCriteria = { hasOpenOrders: false }
58
74
  ) {
59
75
  this.driftClient = driftClient;
60
76
  this.accountSubscription = accountSubscription;
61
77
  this.includeIdle = includeIdle;
78
+ this.syncCallback = syncCallback;
79
+ this.syncCallbackCriteria = syncCallbackCriteria;
80
+ }
81
+
82
+ public addSyncCallback(
83
+ syncCallback?: (authorities: PublicKey[]) => Promise<void>,
84
+ syncCallbackCriteria: SyncCallbackCriteria = { hasOpenOrders: false }
85
+ ) {
86
+ this.syncCallback = syncCallback;
87
+ this.syncCallbackCriteria = syncCallbackCriteria;
62
88
  }
63
89
 
64
90
  public async subscribe() {
@@ -69,7 +95,10 @@ export class UserMap implements UserMapInterface {
69
95
  await this.driftClient.subscribe();
70
96
  this.lastNumberOfSubAccounts =
71
97
  this.driftClient.getStateAccount().numberOfSubAccounts;
72
- this.driftClient.eventEmitter.on('stateAccountUpdate', this.syncCallback);
98
+ this.driftClient.eventEmitter.on(
99
+ 'stateAccountUpdate',
100
+ this.stateAccountUpdateCallback
101
+ );
73
102
 
74
103
  await this.sync();
75
104
  }
@@ -187,74 +216,118 @@ export class UserMap implements UserMapInterface {
187
216
  return this.userMap.size;
188
217
  }
189
218
 
219
+ public getUniqueAuthorities(useSyncCallbackCriteria = true): PublicKey[] {
220
+ const usersMeetingCriteria = Array.from(this.userMap.values()).filter(
221
+ (user) => {
222
+ let pass = true;
223
+ if (
224
+ useSyncCallbackCriteria &&
225
+ this.syncCallbackCriteria.hasOpenOrders
226
+ ) {
227
+ pass = pass && user.getUserAccount().hasOpenOrder;
228
+ }
229
+ return pass;
230
+ }
231
+ );
232
+ const userAuths = new Set(
233
+ usersMeetingCriteria.map((user) =>
234
+ user.getUserAccount().authority.toBase58()
235
+ )
236
+ );
237
+ const userAuthKeys = Array.from(userAuths).map(
238
+ (userAuth) => new PublicKey(userAuth)
239
+ );
240
+ return userAuthKeys;
241
+ }
242
+
190
243
  public async sync() {
191
- const filters = [getUserFilter()];
192
- if (!this.includeIdle) {
193
- filters.push(getNonIdleUserFilter());
244
+ if (this.syncPromise) {
245
+ return this.syncPromise;
194
246
  }
247
+ this.syncPromise = new Promise((resolver) => {
248
+ this.syncPromiseResolver = resolver;
249
+ });
195
250
 
196
- const rpcRequestArgs = [
197
- this.driftClient.program.programId.toBase58(),
198
- {
199
- commitment: this.driftClient.connection.commitment,
200
- filters,
201
- encoding: 'base64',
202
- withContext: true,
203
- },
204
- ];
205
-
206
- // @ts-ignore
207
- const rpcJSONResponse: any = await this.driftClient.connection._rpcRequest(
208
- 'getProgramAccounts',
209
- rpcRequestArgs
210
- );
251
+ try {
252
+ const filters = [getUserFilter()];
253
+ if (!this.includeIdle) {
254
+ filters.push(getNonIdleUserFilter());
255
+ }
211
256
 
212
- const rpcResponseAndContext: RpcResponseAndContext<
213
- Array<{
214
- pubkey: PublicKey;
215
- account: {
216
- data: [string, string];
217
- };
218
- }>
219
- > = rpcJSONResponse.result;
220
-
221
- const slot = rpcResponseAndContext.context.slot;
222
-
223
- const programAccountBufferMap = new Map<string, Buffer>();
224
- for (const programAccount of rpcResponseAndContext.value) {
225
- programAccountBufferMap.set(
226
- programAccount.pubkey.toString(),
257
+ const rpcRequestArgs = [
258
+ this.driftClient.program.programId.toBase58(),
259
+ {
260
+ commitment: this.driftClient.connection.commitment,
261
+ filters,
262
+ encoding: 'base64',
263
+ withContext: true,
264
+ },
265
+ ];
266
+
267
+ const rpcJSONResponse: any =
227
268
  // @ts-ignore
228
- Buffer.from(
229
- programAccount.account.data[0],
230
- programAccount.account.data[1]
231
- )
232
- );
233
- }
269
+ await this.driftClient.connection._rpcRequest(
270
+ 'getProgramAccounts',
271
+ rpcRequestArgs
272
+ );
273
+
274
+ const rpcResponseAndContext: RpcResponseAndContext<
275
+ Array<{
276
+ pubkey: PublicKey;
277
+ account: {
278
+ data: [string, string];
279
+ };
280
+ }>
281
+ > = rpcJSONResponse.result;
282
+
283
+ const slot = rpcResponseAndContext.context.slot;
234
284
 
235
- for (const [key, buffer] of programAccountBufferMap.entries()) {
236
- if (!this.has(key)) {
237
- const userAccount =
238
- this.driftClient.program.account.user.coder.accounts.decode(
239
- 'User',
240
- buffer
241
- );
242
- await this.addPubkey(new PublicKey(key), userAccount);
285
+ const programAccountBufferMap = new Map<string, Buffer>();
286
+ for (const programAccount of rpcResponseAndContext.value) {
287
+ programAccountBufferMap.set(
288
+ programAccount.pubkey.toString(),
289
+ // @ts-ignore
290
+ Buffer.from(
291
+ programAccount.account.data[0],
292
+ programAccount.account.data[1]
293
+ )
294
+ );
243
295
  }
244
- }
245
296
 
246
- for (const [key, user] of this.userMap.entries()) {
247
- if (!programAccountBufferMap.has(key)) {
248
- await user.unsubscribe();
249
- this.userMap.delete(key);
250
- } else {
251
- const userAccount =
252
- this.driftClient.program.account.user.coder.accounts.decode(
253
- 'User',
254
- programAccountBufferMap.get(key)
255
- );
256
- user.accountSubscriber.updateData(userAccount, slot);
297
+ for (const [key, buffer] of programAccountBufferMap.entries()) {
298
+ if (!this.has(key)) {
299
+ const userAccount =
300
+ this.driftClient.program.account.user.coder.accounts.decode(
301
+ 'User',
302
+ buffer
303
+ );
304
+ await this.addPubkey(new PublicKey(key), userAccount);
305
+ }
306
+ }
307
+
308
+ for (const [key, user] of this.userMap.entries()) {
309
+ if (!programAccountBufferMap.has(key)) {
310
+ await user.unsubscribe();
311
+ this.userMap.delete(key);
312
+ } else {
313
+ const userAccount =
314
+ this.driftClient.program.account.user.coder.accounts.decode(
315
+ 'User',
316
+ programAccountBufferMap.get(key)
317
+ );
318
+ user.accountSubscriber.updateData(userAccount, slot);
319
+ }
320
+ }
321
+
322
+ if (this.syncCallback) {
323
+ await this.syncCallback(this.getUniqueAuthorities());
257
324
  }
325
+ } catch (e) {
326
+ console.error(`Error in UserMap.sync()`);
327
+ console.error(e);
328
+ } finally {
329
+ this.syncPromiseResolver();
330
+ this.syncPromise = undefined;
258
331
  }
259
332
  }
260
333
 
@@ -267,7 +340,7 @@ export class UserMap implements UserMapInterface {
267
340
  if (this.lastNumberOfSubAccounts) {
268
341
  this.driftClient.eventEmitter.removeListener(
269
342
  'stateAccountUpdate',
270
- this.syncCallback
343
+ this.stateAccountUpdateCallback
271
344
  );
272
345
  this.lastNumberOfSubAccounts = undefined;
273
346
  }
@@ -4,7 +4,6 @@ import {
4
4
  OrderRecord,
5
5
  UserStatsAccount,
6
6
  UserStats,
7
- UserStatsSubscriptionConfig,
8
7
  WrappedEvent,
9
8
  DepositRecord,
10
9
  FundingPaymentRecord,
@@ -14,12 +13,12 @@ import {
14
13
  NewUserRecord,
15
14
  LPRecord,
16
15
  InsuranceFundStakeRecord,
17
- StateAccount,
16
+ BulkAccountLoader,
17
+ PollingUserStatsAccountSubscriber,
18
18
  } from '..';
19
- import { AccountInfo, PublicKey } from '@solana/web3.js';
19
+ import { PublicKey } from '@solana/web3.js';
20
20
 
21
21
  import { UserMap } from './userMap';
22
- import { Buffer } from 'buffer';
23
22
 
24
23
  export class UserStatsMap {
25
24
  /**
@@ -27,39 +26,45 @@ export class UserStatsMap {
27
26
  */
28
27
  private userStatsMap = new Map<string, UserStats>();
29
28
  private driftClient: DriftClient;
30
- private accountSubscription: UserStatsSubscriptionConfig;
31
- private lastNumberOfAuthorities;
32
- private syncCallback = async (state: StateAccount) => {
33
- if (state.numberOfAuthorities !== this.lastNumberOfAuthorities) {
34
- await this.sync();
35
- this.lastNumberOfAuthorities = state.numberOfAuthorities;
36
- }
37
- };
29
+ private bulkAccountLoader: BulkAccountLoader;
38
30
 
39
- constructor(
40
- driftClient: DriftClient,
41
- accountSubscription: UserStatsSubscriptionConfig
42
- ) {
31
+ /**
32
+ * Creates a new UserStatsMap instance.
33
+ *
34
+ * @param {DriftClient} driftClient - The DriftClient instance.
35
+ * @param {BulkAccountLoader} [bulkAccountLoader] - If not provided, a new BulkAccountLoader with polling disabled will be created.
36
+ */
37
+ constructor(driftClient: DriftClient, bulkAccountLoader?: BulkAccountLoader) {
43
38
  this.driftClient = driftClient;
44
- this.accountSubscription = accountSubscription;
39
+ if (!bulkAccountLoader) {
40
+ bulkAccountLoader = new BulkAccountLoader(
41
+ driftClient.connection,
42
+ driftClient.opts.commitment,
43
+ 0
44
+ );
45
+ }
46
+ this.bulkAccountLoader = bulkAccountLoader;
45
47
  }
46
48
 
47
- public async subscribe() {
49
+ public async subscribe(authorities: PublicKey[]) {
48
50
  if (this.size() > 0) {
49
51
  return;
50
52
  }
51
53
 
52
54
  await this.driftClient.subscribe();
53
- this.lastNumberOfAuthorities =
54
- this.driftClient.getStateAccount().numberOfAuthorities;
55
- this.driftClient.eventEmitter.on('stateAccountUpdate', this.syncCallback);
56
-
57
- await this.sync();
55
+ await this.sync(authorities);
58
56
  }
59
57
 
58
+ /**
59
+ *
60
+ * @param authority that owns the UserStatsAccount
61
+ * @param userStatsAccount optional UserStatsAccount to subscribe to, if undefined will be fetched later
62
+ * @param skipFetch if true, will not immediately fetch the UserStatsAccount
63
+ */
60
64
  public async addUserStat(
61
65
  authority: PublicKey,
62
- userStatsAccount?: UserStatsAccount
66
+ userStatsAccount?: UserStatsAccount,
67
+ skipFetch?: boolean
63
68
  ) {
64
69
  const userStat = new UserStats({
65
70
  driftClient: this.driftClient,
@@ -67,9 +72,18 @@ export class UserStatsMap {
67
72
  this.driftClient.program.programId,
68
73
  authority
69
74
  ),
70
- accountSubscription: this.accountSubscription,
75
+ accountSubscription: {
76
+ type: 'polling',
77
+ accountLoader: this.bulkAccountLoader,
78
+ },
71
79
  });
72
- await userStat.subscribe(userStatsAccount);
80
+ if (skipFetch) {
81
+ await (
82
+ userStat.accountSubscriber as PollingUserStatsAccountSubscriber
83
+ ).addToAccountLoader();
84
+ } else {
85
+ await userStat.subscribe(userStatsAccount);
86
+ }
73
87
 
74
88
  this.userStatsMap.set(authority.toString(), userStat);
75
89
  }
@@ -77,7 +91,7 @@ export class UserStatsMap {
77
91
  public async updateWithOrderRecord(record: OrderRecord, userMap: UserMap) {
78
92
  const user = await userMap.mustGet(record.user.toString());
79
93
  if (!this.has(user.getUserAccount().authority.toString())) {
80
- await this.addUserStat(user.getUserAccount().authority);
94
+ await this.addUserStat(user.getUserAccount().authority, undefined, false);
81
95
  }
82
96
  }
83
97
 
@@ -156,9 +170,19 @@ export class UserStatsMap {
156
170
  return this.userStatsMap.get(authorityPublicKey);
157
171
  }
158
172
 
173
+ /**
174
+ * Enforce that a UserStats will exist for the given authorityPublicKey,
175
+ * reading one from the blockchain if necessary.
176
+ * @param authorityPublicKey
177
+ * @returns
178
+ */
159
179
  public async mustGet(authorityPublicKey: string): Promise<UserStats> {
160
180
  if (!this.has(authorityPublicKey)) {
161
- await this.addUserStat(new PublicKey(authorityPublicKey));
181
+ await this.addUserStat(
182
+ new PublicKey(authorityPublicKey),
183
+ undefined,
184
+ false
185
+ );
162
186
  }
163
187
  return this.get(authorityPublicKey);
164
188
  }
@@ -171,39 +195,18 @@ export class UserStatsMap {
171
195
  return this.userStatsMap.size;
172
196
  }
173
197
 
174
- public async sync() {
175
- const programAccounts =
176
- await this.driftClient.connection.getProgramAccounts(
177
- this.driftClient.program.programId,
178
- {
179
- commitment: this.driftClient.connection.commitment,
180
- filters: [
181
- {
182
- memcmp:
183
- this.driftClient.program.coder.accounts.memcmp('UserStats'),
184
- },
185
- ],
186
- }
187
- );
188
-
189
- const programAccountMap = new Map<string, AccountInfo<Buffer>>();
190
- for (const programAccount of programAccounts) {
191
- programAccountMap.set(
192
- new PublicKey(programAccount.account.data.slice(8, 40)).toString(),
193
- programAccount.account
194
- );
195
- }
196
-
197
- for (const key of programAccountMap.keys()) {
198
- if (!this.has(key)) {
199
- const userStatsAccount =
200
- this.driftClient.program.account.userStats.coder.accounts.decode(
201
- 'UserStats',
202
- programAccountMap.get(key).data
203
- );
204
- await this.addUserStat(new PublicKey(key), userStatsAccount);
205
- }
206
- }
198
+ /**
199
+ * Sync the UserStatsMap
200
+ * @param authorities list of authorities to derive UserStatsAccount public keys from.
201
+ * You may want to get this list from UserMap in order to filter out idle users
202
+ */
203
+ public async sync(authorities: PublicKey[]) {
204
+ await Promise.all(
205
+ authorities.map((authority) =>
206
+ this.addUserStat(authority, undefined, true)
207
+ )
208
+ );
209
+ await this.bulkAccountLoader.load();
207
210
  }
208
211
 
209
212
  public async unsubscribe() {
@@ -211,13 +214,5 @@ export class UserStatsMap {
211
214
  await userStats.unsubscribe();
212
215
  this.userStatsMap.delete(key);
213
216
  }
214
-
215
- if (this.lastNumberOfAuthorities) {
216
- this.driftClient.eventEmitter.removeListener(
217
- 'stateAccountUpdate',
218
- this.syncCallback
219
- );
220
- this.lastNumberOfAuthorities = undefined;
221
- }
222
217
  }
223
218
  }
package/tests/amm/test.ts CHANGED
@@ -967,8 +967,9 @@ describe('AMM Tests', () => {
967
967
  mockMarket1.amm.maxBaseAssetReserve = mockMarket1.amm.baseAssetReserve.add(
968
968
  new BN(9)
969
969
  );
970
- mockMarket1.amm.minBaseAssetReserve =
971
- mockMarket1.amm.baseAssetReserve.sub(new BN(9));
970
+ mockMarket1.amm.minBaseAssetReserve = mockMarket1.amm.baseAssetReserve.sub(
971
+ new BN(9)
972
+ );
972
973
  mockMarket1.amm.quoteAssetReserve = new BN(cc).mul(BASE_PRECISION);
973
974
  mockMarket1.amm.pegMultiplier = new BN(18.32 * PEG_PRECISION.toNumber());
974
975
  mockMarket1.amm.sqrtK = new BN(cc).mul(BASE_PRECISION);
@@ -27,7 +27,7 @@ import {
27
27
 
28
28
  import { mockPerpMarkets, mockSpotMarkets, mockStateAccount } from './helpers';
29
29
  import { DLOBOrdersCoder } from '../../src/dlob/DLOBOrders';
30
- import {isAuctionComplete, isRestingLimitOrder} from "../../lib";
30
+ import { isAuctionComplete, isRestingLimitOrder } from '../../lib';
31
31
 
32
32
  function insertOrderToDLOB(
33
33
  dlob: DLOB,
@@ -2537,18 +2537,21 @@ describe('DLOB Perp Tests', () => {
2537
2537
  OrderTriggerCondition.TRIGGERED_ABOVE, // triggerCondition: OrderTriggerCondition,
2538
2538
  vBid,
2539
2539
  vAsk,
2540
- new BN(1), // slot
2540
+ new BN(1) // slot
2541
2541
  );
2542
2542
 
2543
- const restingLimitBids = Array.from(dlob.getRestingLimitBids(marketIndex, slot, MarketType.PERP, oracle));
2543
+ const restingLimitBids = Array.from(
2544
+ dlob.getRestingLimitBids(marketIndex, slot, MarketType.PERP, oracle)
2545
+ );
2544
2546
  expect(restingLimitBids.length).to.equal(0);
2545
2547
 
2546
- const takingBids = Array.from(dlob.getTakingBids(marketIndex, MarketType.PERP, slot, oracle));
2548
+ const takingBids = Array.from(
2549
+ dlob.getTakingBids(marketIndex, MarketType.PERP, slot, oracle)
2550
+ );
2547
2551
  expect(takingBids.length).to.equal(1);
2548
2552
  const triggerLimitBid = takingBids[0];
2549
2553
  expect(isAuctionComplete(triggerLimitBid.order, slot)).to.equal(true);
2550
2554
  expect(isRestingLimitOrder(triggerLimitBid.order, slot)).to.equal(false);
2551
-
2552
2555
  });
2553
2556
 
2554
2557
  it('Test will return expired market orders to fill', () => {