@drift-labs/sdk
Version:
SDK for Drift Protocol
276 lines (275 loc) • 11.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserStatsMap = void 0;
const pda_1 = require("../addresses/pda");
const userStats_1 = require("../userStats");
const bulkAccountLoader_1 = require("../accounts/bulkAccountLoader");
const memcmp_1 = require("../memcmp");
const web3_js_1 = require("@solana/web3.js");
class UserStatsMap {
/**
* Creates a new UserStatsMap instance.
*
* @param {DriftClient} driftClient - The DriftClient instance.
* @param {BulkAccountLoader} [bulkAccountLoader] - If not provided, a new BulkAccountLoader with polling disabled will be created.
*/
constructor(driftClient, bulkAccountLoader, syncConfig) {
/**
* map from authority pubkey to UserStats
*/
this.userStatsMap = new Map();
this.driftClient = driftClient;
if (!bulkAccountLoader) {
bulkAccountLoader = new bulkAccountLoader_1.BulkAccountLoader(driftClient.connection, driftClient.opts.commitment, 0);
}
this.bulkAccountLoader = bulkAccountLoader;
this.syncConfig = syncConfig !== null && syncConfig !== void 0 ? syncConfig : {
type: 'default',
};
this.decode =
this.driftClient.program.account.userStats.coder.accounts.decodeUnchecked.bind(this.driftClient.program.account.userStats.coder.accounts);
}
async subscribe(authorities) {
if (this.size() > 0) {
return;
}
await this.driftClient.subscribe();
await this.sync(authorities);
}
/**
*
* @param authority that owns the UserStatsAccount
* @param userStatsAccount optional UserStatsAccount to subscribe to, if undefined will be fetched later
* @param skipFetch if true, will not immediately fetch the UserStatsAccount
*/
async addUserStat(authority, userStatsAccount, skipFetch) {
const userStat = new userStats_1.UserStats({
driftClient: this.driftClient,
userStatsAccountPublicKey: (0, pda_1.getUserStatsAccountPublicKey)(this.driftClient.program.programId, authority),
accountSubscription: {
type: 'polling',
accountLoader: this.bulkAccountLoader,
},
});
if (skipFetch) {
await userStat.accountSubscriber.addToAccountLoader();
}
else {
await userStat.subscribe(userStatsAccount);
}
this.userStatsMap.set(authority.toString(), userStat);
}
async updateWithOrderRecord(record, userMap) {
const user = await userMap.mustGet(record.user.toString());
if (!this.has(user.getUserAccount().authority.toString())) {
await this.addUserStat(user.getUserAccount().authority, undefined, false);
}
}
async updateWithEventRecord(record, userMap) {
if (record.eventType === 'DepositRecord') {
const depositRecord = record;
await this.mustGet(depositRecord.userAuthority.toString());
}
else if (record.eventType === 'FundingPaymentRecord') {
const fundingPaymentRecord = record;
await this.mustGet(fundingPaymentRecord.userAuthority.toString());
}
else if (record.eventType === 'LiquidationRecord') {
if (!userMap) {
return;
}
const liqRecord = record;
const user = await userMap.mustGet(liqRecord.user.toString());
await this.mustGet(user.getUserAccount().authority.toString());
const liquidatorUser = await userMap.mustGet(liqRecord.liquidator.toString());
await this.mustGet(liquidatorUser.getUserAccount().authority.toString());
}
else if (record.eventType === 'OrderRecord') {
if (!userMap) {
return;
}
const orderRecord = record;
await userMap.updateWithOrderRecord(orderRecord);
}
else if (record.eventType === 'OrderActionRecord') {
if (!userMap) {
return;
}
const actionRecord = record;
if (actionRecord.taker) {
const taker = await userMap.mustGet(actionRecord.taker.toString());
await this.mustGet(taker.getUserAccount().authority.toString());
}
if (actionRecord.maker) {
const maker = await userMap.mustGet(actionRecord.maker.toString());
await this.mustGet(maker.getUserAccount().authority.toString());
}
}
else if (record.eventType === 'SettlePnlRecord') {
if (!userMap) {
return;
}
const settlePnlRecord = record;
const user = await userMap.mustGet(settlePnlRecord.user.toString());
await this.mustGet(user.getUserAccount().authority.toString());
}
else if (record.eventType === 'NewUserRecord') {
const newUserRecord = record;
await this.mustGet(newUserRecord.userAuthority.toString());
}
else if (record.eventType === 'LPRecord') {
if (!userMap) {
return;
}
const lpRecord = record;
const user = await userMap.mustGet(lpRecord.user.toString());
await this.mustGet(user.getUserAccount().authority.toString());
}
else if (record.eventType === 'InsuranceFundStakeRecord') {
const ifStakeRecord = record;
await this.mustGet(ifStakeRecord.userAuthority.toString());
}
}
has(authorityPublicKey) {
return this.userStatsMap.has(authorityPublicKey);
}
get(authorityPublicKey) {
return this.userStatsMap.get(authorityPublicKey);
}
/**
* Enforce that a UserStats will exist for the given authorityPublicKey,
* reading one from the blockchain if necessary.
* @param authorityPublicKey
* @returns
*/
async mustGet(authorityPublicKey) {
if (!this.has(authorityPublicKey)) {
await this.addUserStat(new web3_js_1.PublicKey(authorityPublicKey), undefined, false);
}
return this.get(authorityPublicKey);
}
values() {
return this.userStatsMap.values();
}
size() {
return this.userStatsMap.size;
}
/**
* Sync the UserStatsMap
* @param authorities list of authorities to derive UserStatsAccount public keys from.
* You may want to get this list from UserMap in order to filter out idle users
*/
async sync(authorities) {
if (this.syncConfig.type === 'default') {
return this.defaultSync(authorities);
}
else {
return this.paginatedSync(authorities);
}
}
/**
* Sync the UserStatsMap using the default sync method, which loads individual users into the bulkAccountLoader and
* loads them. (bulkAccountLoader uses batch getMultipleAccounts)
* @param authorities
*/
async defaultSync(authorities) {
await Promise.all(authorities.map((authority) => this.addUserStat(authority, undefined, true)));
await this.bulkAccountLoader.load();
}
/**
* Sync the UserStatsMap using the paginated sync method, which uses multiple getMultipleAccounts calls (without RPC batching), and limits concurrency.
* @param authorities
*/
async paginatedSync(authorities) {
var _a, _b;
if (this.syncPromise) {
return this.syncPromise;
}
this.syncPromise = new Promise((resolve) => {
this.syncPromiseResolver = resolve;
});
try {
let accountsToLoad = authorities;
if (authorities.length === 0) {
const accountsPrefetch = await this.driftClient.connection.getProgramAccounts(this.driftClient.program.programId, {
dataSlice: { offset: 0, length: 0 },
filters: [(0, memcmp_1.getUserStatsFilter)()],
});
accountsToLoad = accountsPrefetch.map((account) => account.pubkey);
}
const limitConcurrency = async (tasks, limit) => {
const executing = [];
const results = [];
for (let i = 0; i < tasks.length; i++) {
const executor = Promise.resolve().then(tasks[i]);
results.push(executor);
if (executing.length < limit) {
executing.push(executor);
executor.finally(() => {
const index = executing.indexOf(executor);
if (index > -1) {
executing.splice(index, 1);
}
});
}
else {
await Promise.race(executing);
}
}
return Promise.all(results);
};
const programAccountBufferMap = new Set();
// @ts-ignore
const chunkSize = (_a = this.syncConfig.chunkSize) !== null && _a !== void 0 ? _a : 100;
const tasks = [];
for (let i = 0; i < accountsToLoad.length; i += chunkSize) {
const chunk = accountsToLoad.slice(i, i + chunkSize);
tasks.push(async () => {
const accountInfos = await this.driftClient.connection.getMultipleAccountsInfoAndContext(chunk, {
commitment: this.driftClient.opts.commitment,
});
for (let j = 0; j < accountInfos.value.length; j += 1) {
const accountInfo = accountInfos.value[j];
if (accountInfo === null)
continue;
const publicKeyString = chunk[j].toString();
if (!this.has(publicKeyString)) {
const buffer = Buffer.from(accountInfo.data);
const decodedUserStats = this.decode('UserStats', buffer);
programAccountBufferMap.add(decodedUserStats.authority.toBase58());
this.addUserStat(decodedUserStats.authority, decodedUserStats, false);
}
}
});
}
// @ts-ignore
const concurrencyLimit = (_b = this.syncConfig.concurrencyLimit) !== null && _b !== void 0 ? _b : 10;
await limitConcurrency(tasks, concurrencyLimit);
for (const [key] of this.userStatsMap.entries()) {
if (!programAccountBufferMap.has(key)) {
const user = this.get(key);
if (user) {
await user.unsubscribe();
this.userStatsMap.delete(key);
}
}
}
}
catch (err) {
console.error(`Error in UserStatsMap.paginatedSync():`, err);
}
finally {
if (this.syncPromiseResolver) {
this.syncPromiseResolver();
}
this.syncPromise = undefined;
}
}
async unsubscribe() {
for (const [key, userStats] of this.userStatsMap.entries()) {
await userStats.unsubscribe();
this.userStatsMap.delete(key);
}
}
}
exports.UserStatsMap = UserStatsMap;