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

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.2
1
+ 2.48.0-beta.3
@@ -53,12 +53,12 @@ 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
+ const dataAndContext = await this.program.account.user.fetchAndContext(this.userAccountPublicKey, this.accountLoader.commitment);
57
+ if ((_b = dataAndContext.context.slot > ((_a = this.user) === null || _a === void 0 ? void 0 : _a.slot)) !== null && _b !== void 0 ? _b : 0) {
58
+ this.user = {
59
+ data: dataAndContext.data,
60
+ slot: dataAndContext.context.slot,
61
+ };
62
62
  }
63
63
  }
64
64
  doesAccountExist() {
@@ -80,7 +80,9 @@ class PollingUserAccountSubscriber {
80
80
  }
81
81
  }
82
82
  getUserAccountAndSlot() {
83
- this.assertIsSubscribed();
83
+ if (!this.doesAccountExist()) {
84
+ throw new types_1.NotSubscribedError('You must call `subscribe` or `fetch` before using this function');
85
+ }
84
86
  return this.user;
85
87
  }
86
88
  updateData(userAccount, slot) {
@@ -53,12 +53,12 @@ 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
+ const dataAndContext = await this.program.account.userStats.fetchAndContext(this.userStatsAccountPublicKey, this.accountLoader.commitment);
57
+ if ((_b = dataAndContext.context.slot > ((_a = this.userStats) === null || _a === void 0 ? void 0 : _a.slot)) !== null && _b !== void 0 ? _b : 0) {
58
+ this.userStats = {
59
+ data: dataAndContext.data,
60
+ slot: dataAndContext.context.slot,
61
+ };
62
62
  }
63
63
  }
64
64
  doesAccountExist() {
@@ -80,7 +80,9 @@ class PollingUserStatsAccountSubscriber {
80
80
  }
81
81
  }
82
82
  getUserStatsAccountAndSlot() {
83
- this.assertIsSubscribed();
83
+ if (!this.doesAccountExist()) {
84
+ throw new types_1.NotSubscribedError('You must call `subscribe` or `fetch` before using this function');
85
+ }
84
86
  return this.userStats;
85
87
  }
86
88
  }
@@ -11,20 +11,30 @@ 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);
28
38
  subscribe(): Promise<void>;
29
39
  addPubkey(userAccountPublicKey: PublicKey, userAccount?: UserAccount): Promise<void>;
30
40
  has(key: string): boolean;
@@ -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,8 @@ 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;
26
31
  }
27
32
  async subscribe() {
28
33
  if (this.size() > 0) {
@@ -31,7 +36,7 @@ class UserMap {
31
36
  await this.driftClient.subscribe();
32
37
  this.lastNumberOfSubAccounts =
33
38
  this.driftClient.getStateAccount().numberOfSubAccounts;
34
- this.driftClient.eventEmitter.on('stateAccountUpdate', this.syncCallback);
39
+ this.driftClient.eventEmitter.on('stateAccountUpdate', this.stateAccountUpdateCallback);
35
40
  await this.sync();
36
41
  }
37
42
  async addPubkey(userAccountPublicKey, userAccount) {
@@ -140,44 +145,73 @@ class UserMap {
140
145
  return this.userMap.size;
141
146
  }
142
147
  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]));
148
+ if (this.syncPromise) {
149
+ return this.syncPromise;
165
150
  }
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);
151
+ this.syncPromise = new Promise((resolver) => {
152
+ this.syncPromiseResolver = resolver;
153
+ });
154
+ try {
155
+ const filters = [(0, memcmp_1.getUserFilter)()];
156
+ if (!this.includeIdle) {
157
+ filters.push((0, memcmp_1.getNonIdleUserFilter)());
170
158
  }
171
- }
172
- for (const [key, user] of this.userMap.entries()) {
173
- if (!programAccountBufferMap.has(key)) {
174
- await user.unsubscribe();
175
- this.userMap.delete(key);
159
+ const rpcRequestArgs = [
160
+ this.driftClient.program.programId.toBase58(),
161
+ {
162
+ commitment: this.driftClient.connection.commitment,
163
+ filters,
164
+ encoding: 'base64',
165
+ withContext: true,
166
+ },
167
+ ];
168
+ const rpcJSONResponse =
169
+ // @ts-ignore
170
+ await this.driftClient.connection._rpcRequest('getProgramAccounts', rpcRequestArgs);
171
+ const rpcResponseAndContext = rpcJSONResponse.result;
172
+ const slot = rpcResponseAndContext.context.slot;
173
+ const programAccountBufferMap = new Map();
174
+ for (const programAccount of rpcResponseAndContext.value) {
175
+ programAccountBufferMap.set(programAccount.pubkey.toString(),
176
+ // @ts-ignore
177
+ buffer_1.Buffer.from(programAccount.account.data[0], programAccount.account.data[1]));
176
178
  }
177
- else {
178
- const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', programAccountBufferMap.get(key));
179
- user.accountSubscriber.updateData(userAccount, slot);
179
+ for (const [key, buffer] of programAccountBufferMap.entries()) {
180
+ if (!this.has(key)) {
181
+ const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', buffer);
182
+ await this.addPubkey(new web3_js_1.PublicKey(key), userAccount);
183
+ }
180
184
  }
185
+ for (const [key, user] of this.userMap.entries()) {
186
+ if (!programAccountBufferMap.has(key)) {
187
+ await user.unsubscribe();
188
+ this.userMap.delete(key);
189
+ }
190
+ else {
191
+ const userAccount = this.driftClient.program.account.user.coder.accounts.decode('User', programAccountBufferMap.get(key));
192
+ user.accountSubscriber.updateData(userAccount, slot);
193
+ }
194
+ }
195
+ if (this.syncCallback) {
196
+ const usersMeetingCriteria = Array.from(this.userMap.values()).filter((user) => {
197
+ let pass = true;
198
+ if (this.syncCallbackCriteria.hasOpenOrders) {
199
+ pass = pass && user.getUserAccount().hasOpenOrder;
200
+ }
201
+ return pass;
202
+ });
203
+ const userAuths = new Set(usersMeetingCriteria.map((user) => user.getUserAccount().authority.toBase58()));
204
+ const userAuthKeys = Array.from(userAuths).map((userAuth) => new web3_js_1.PublicKey(userAuth));
205
+ await this.syncCallback(userAuthKeys);
206
+ }
207
+ }
208
+ catch (e) {
209
+ console.error(`Error in UserMap.sync()`);
210
+ console.error(e);
211
+ }
212
+ finally {
213
+ this.syncPromiseResolver();
214
+ this.syncPromise = undefined;
181
215
  }
182
216
  }
183
217
  async unsubscribe() {
@@ -186,7 +220,7 @@ class UserMap {
186
220
  this.userMap.delete(key);
187
221
  }
188
222
  if (this.lastNumberOfSubAccounts) {
189
- this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.syncCallback);
223
+ this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.stateAccountUpdateCallback);
190
224
  this.lastNumberOfSubAccounts = undefined;
191
225
  }
192
226
  }
@@ -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,21 @@ 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
+ console.log(`USER MAP SIYCING AUTHS: ${authorities.length}`);
156
+ await Promise.all(authorities.map((authority) => this.addUserStat(authority, undefined, true)));
157
+ await this.bulkAccountLoader.load();
148
158
  }
149
159
  async unsubscribe() {
150
160
  for (const [key, userStats] of this.userStatsMap.entries()) {
151
161
  await userStats.unsubscribe();
152
162
  this.userStatsMap.delete(key);
153
163
  }
154
- if (this.lastNumberOfAuthorities) {
155
- this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.syncCallback);
156
- this.lastNumberOfAuthorities = undefined;
157
- }
158
164
  }
159
165
  }
160
166
  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.2",
3
+ "version": "2.48.0-beta.3",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "author": "crispheaney",
@@ -93,17 +93,15 @@ 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
96
+ const dataAndContext = await this.program.account.user.fetchAndContext(
97
+ this.userAccountPublicKey,
98
+ this.accountLoader.commitment
99
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
105
- );
106
- this.user = { data: account, slot };
100
+ if (dataAndContext.context.slot > this.user?.slot ?? 0) {
101
+ this.user = {
102
+ data: dataAndContext.data as UserAccount,
103
+ slot: dataAndContext.context.slot,
104
+ };
107
105
  }
108
106
  }
109
107
 
@@ -137,7 +135,11 @@ export class PollingUserAccountSubscriber implements UserAccountSubscriber {
137
135
  }
138
136
 
139
137
  public getUserAccountAndSlot(): DataAndSlot<UserAccount> {
140
- this.assertIsSubscribed();
138
+ if (!this.doesAccountExist()) {
139
+ throw new NotSubscribedError(
140
+ 'You must call `subscribe` or `fetch` before using this function'
141
+ );
142
+ }
141
143
  return this.user;
142
144
  }
143
145
 
@@ -97,18 +97,15 @@ 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
100
+ const dataAndContext = await this.program.account.userStats.fetchAndContext(
101
+ this.userStatsAccountPublicKey,
102
+ this.accountLoader.commitment
103
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
110
- );
111
- this.userStats = { data: account, slot };
104
+ if (dataAndContext.context.slot > this.userStats?.slot ?? 0) {
105
+ this.userStats = {
106
+ data: dataAndContext.data as UserStatsAccount,
107
+ slot: dataAndContext.context.slot,
108
+ };
112
109
  }
113
110
  }
114
111
 
@@ -142,7 +139,11 @@ export class PollingUserStatsAccountSubscriber
142
139
  }
143
140
 
144
141
  public getUserStatsAccountAndSlot(): DataAndSlot<UserStatsAccount> {
145
- this.assertIsSubscribed();
142
+ if (!this.doesAccountExist()) {
143
+ throw new NotSubscribedError(
144
+ 'You must call `subscribe` or `fetch` before using this function'
145
+ );
146
+ }
146
147
  return this.userStats;
147
148
  }
148
149
  }
@@ -32,33 +32,51 @@ 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;
62
80
  }
63
81
 
64
82
  public async subscribe() {
@@ -69,7 +87,10 @@ export class UserMap implements UserMapInterface {
69
87
  await this.driftClient.subscribe();
70
88
  this.lastNumberOfSubAccounts =
71
89
  this.driftClient.getStateAccount().numberOfSubAccounts;
72
- this.driftClient.eventEmitter.on('stateAccountUpdate', this.syncCallback);
90
+ this.driftClient.eventEmitter.on(
91
+ 'stateAccountUpdate',
92
+ this.stateAccountUpdateCallback
93
+ );
73
94
 
74
95
  await this.sync();
75
96
  }
@@ -188,73 +209,110 @@ export class UserMap implements UserMapInterface {
188
209
  }
189
210
 
190
211
  public async sync() {
191
- const filters = [getUserFilter()];
192
- if (!this.includeIdle) {
193
- filters.push(getNonIdleUserFilter());
212
+ if (this.syncPromise) {
213
+ return this.syncPromise;
194
214
  }
215
+ this.syncPromise = new Promise((resolver) => {
216
+ this.syncPromiseResolver = resolver;
217
+ });
195
218
 
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
- );
219
+ try {
220
+ const filters = [getUserFilter()];
221
+ if (!this.includeIdle) {
222
+ filters.push(getNonIdleUserFilter());
223
+ }
224
+
225
+ const rpcRequestArgs = [
226
+ this.driftClient.program.programId.toBase58(),
227
+ {
228
+ commitment: this.driftClient.connection.commitment,
229
+ filters,
230
+ encoding: 'base64',
231
+ withContext: true,
232
+ },
233
+ ];
211
234
 
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(),
235
+ const rpcJSONResponse: any =
227
236
  // @ts-ignore
228
- Buffer.from(
229
- programAccount.account.data[0],
230
- programAccount.account.data[1]
231
- )
232
- );
233
- }
237
+ await this.driftClient.connection._rpcRequest(
238
+ 'getProgramAccounts',
239
+ rpcRequestArgs
240
+ );
234
241
 
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);
242
+ const rpcResponseAndContext: RpcResponseAndContext<
243
+ Array<{
244
+ pubkey: PublicKey;
245
+ account: {
246
+ data: [string, string];
247
+ };
248
+ }>
249
+ > = rpcJSONResponse.result;
250
+
251
+ const slot = rpcResponseAndContext.context.slot;
252
+
253
+ const programAccountBufferMap = new Map<string, Buffer>();
254
+ for (const programAccount of rpcResponseAndContext.value) {
255
+ programAccountBufferMap.set(
256
+ programAccount.pubkey.toString(),
257
+ // @ts-ignore
258
+ Buffer.from(
259
+ programAccount.account.data[0],
260
+ programAccount.account.data[1]
261
+ )
262
+ );
243
263
  }
244
- }
245
264
 
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);
265
+ for (const [key, buffer] of programAccountBufferMap.entries()) {
266
+ if (!this.has(key)) {
267
+ const userAccount =
268
+ this.driftClient.program.account.user.coder.accounts.decode(
269
+ 'User',
270
+ buffer
271
+ );
272
+ await this.addPubkey(new PublicKey(key), userAccount);
273
+ }
274
+ }
275
+
276
+ for (const [key, user] of this.userMap.entries()) {
277
+ if (!programAccountBufferMap.has(key)) {
278
+ await user.unsubscribe();
279
+ this.userMap.delete(key);
280
+ } else {
281
+ const userAccount =
282
+ this.driftClient.program.account.user.coder.accounts.decode(
283
+ 'User',
284
+ programAccountBufferMap.get(key)
285
+ );
286
+ user.accountSubscriber.updateData(userAccount, slot);
287
+ }
288
+ }
289
+
290
+ if (this.syncCallback) {
291
+ const usersMeetingCriteria = Array.from(this.userMap.values()).filter(
292
+ (user) => {
293
+ let pass = true;
294
+ if (this.syncCallbackCriteria.hasOpenOrders) {
295
+ pass = pass && user.getUserAccount().hasOpenOrder;
296
+ }
297
+ return pass;
298
+ }
299
+ );
300
+ const userAuths = new Set(
301
+ usersMeetingCriteria.map((user) =>
302
+ user.getUserAccount().authority.toBase58()
303
+ )
304
+ );
305
+ const userAuthKeys = Array.from(userAuths).map(
306
+ (userAuth) => new PublicKey(userAuth)
307
+ );
308
+ await this.syncCallback(userAuthKeys);
257
309
  }
310
+ } catch (e) {
311
+ console.error(`Error in UserMap.sync()`);
312
+ console.error(e);
313
+ } finally {
314
+ this.syncPromiseResolver();
315
+ this.syncPromise = undefined;
258
316
  }
259
317
  }
260
318
 
@@ -267,7 +325,7 @@ export class UserMap implements UserMapInterface {
267
325
  if (this.lastNumberOfSubAccounts) {
268
326
  this.driftClient.eventEmitter.removeListener(
269
327
  'stateAccountUpdate',
270
- this.syncCallback
328
+ this.stateAccountUpdateCallback
271
329
  );
272
330
  this.lastNumberOfSubAccounts = undefined;
273
331
  }
@@ -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,19 @@ 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
+ console.log(`USER MAP SIYCING AUTHS: ${authorities.length}`);
205
+ await Promise.all(
206
+ authorities.map((authority) =>
207
+ this.addUserStat(authority, undefined, true)
208
+ )
209
+ );
210
+ await this.bulkAccountLoader.load();
207
211
  }
208
212
 
209
213
  public async unsubscribe() {
@@ -211,13 +215,5 @@ export class UserStatsMap {
211
215
  await userStats.unsubscribe();
212
216
  this.userStatsMap.delete(key);
213
217
  }
214
-
215
- if (this.lastNumberOfAuthorities) {
216
- this.driftClient.eventEmitter.removeListener(
217
- 'stateAccountUpdate',
218
- this.syncCallback
219
- );
220
- this.lastNumberOfAuthorities = undefined;
221
- }
222
218
  }
223
219
  }
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', () => {