@drift-labs/sdk
Version:
SDK for Drift Protocol
492 lines (491 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserMap = void 0;
const user_1 = require("../user");
const DLOB_1 = require("../dlob/DLOB");
const oneShotUserAccountSubscriber_1 = require("../accounts/oneShotUserAccountSubscriber");
const web3_js_1 = require("@solana/web3.js");
const buffer_1 = require("buffer");
const zstddec_1 = require("zstddec");
const memcmp_1 = require("../memcmp");
const WebsocketSubscription_1 = require("./WebsocketSubscription");
const PollingSubscription_1 = require("./PollingSubscription");
const user_2 = require("../decode/user");
const grpcSubscription_1 = require("./grpcSubscription");
const events_1 = require("events");
const MAX_USER_ACCOUNT_SIZE_BYTES = 4376;
class UserMap {
/**
* Constructs a new UserMap instance.
*/
constructor(config) {
var _a, _b, _c, _d, _e, _f;
this.userMap = new Map();
this.stateAccountUpdateCallback = async (state) => {
if (!state.numberOfSubAccounts.eq(this.lastNumberOfSubAccounts)) {
await this.sync();
this.lastNumberOfSubAccounts = state.numberOfSubAccounts;
}
};
this.mostRecentSlot = 0;
this.driftClient = config.driftClient;
if (config.connection) {
this.connection = config.connection;
}
else {
this.connection = this.driftClient.connection;
}
this.commitment =
config.subscriptionConfig.type === 'websocket' ||
config.subscriptionConfig.type === 'polling'
? (_a = config.subscriptionConfig.commitment) !== null && _a !== void 0 ? _a : this.driftClient.opts.commitment
: this.driftClient.opts.commitment;
this.includeIdle = (_b = config.includeIdle) !== null && _b !== void 0 ? _b : false;
this.filterByPoolId = config.filterByPoolId;
this.additionalFilters = config.additionalFilters;
this.disableSyncOnTotalAccountsChange =
(_c = config.disableSyncOnTotalAccountsChange) !== null && _c !== void 0 ? _c : false;
let decodeFn;
if ((_d = config.fastDecode) !== null && _d !== void 0 ? _d : true) {
decodeFn = (name, buffer) => (0, user_2.decodeUser)(buffer);
}
else {
decodeFn =
this.driftClient.program.account.user.coder.accounts.decodeUnchecked.bind(this.driftClient.program.account.user.coder.accounts);
}
this.decode = decodeFn;
if (config.subscriptionConfig.type === 'polling') {
this.subscription = new PollingSubscription_1.PollingSubscription({
userMap: this,
frequency: config.subscriptionConfig.frequency,
skipInitialLoad: config.skipInitialLoad,
});
}
else if (config.subscriptionConfig.type === 'grpc') {
this.subscription = new grpcSubscription_1.grpcSubscription({
userMap: this,
grpcConfigs: config.subscriptionConfig.grpcConfigs,
resubOpts: {
resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs,
logResubMessages: config.subscriptionConfig.logResubMessages,
},
skipInitialLoad: config.skipInitialLoad,
decodeFn,
});
}
else {
this.subscription = new WebsocketSubscription_1.WebsocketSubscription({
userMap: this,
commitment: this.commitment,
resubOpts: {
resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs,
logResubMessages: config.subscriptionConfig.logResubMessages,
},
skipInitialLoad: config.skipInitialLoad,
decodeFn,
});
}
this.syncConfig = (_e = config.syncConfig) !== null && _e !== void 0 ? _e : {
type: 'default',
};
// Whether to throw an error if the userMap fails to sync. Defaults to false.
this.throwOnFailedSync = (_f = config.throwOnFailedSync) !== null && _f !== void 0 ? _f : false;
this.eventEmitter = new events_1.EventEmitter();
}
async subscribe() {
if (this.size() > 0) {
return;
}
await this.driftClient.subscribe();
this.lastNumberOfSubAccounts =
this.driftClient.getStateAccount().numberOfSubAccounts;
if (!this.disableSyncOnTotalAccountsChange) {
this.driftClient.eventEmitter.on('stateAccountUpdate', this.stateAccountUpdateCallback);
}
await this.subscription.subscribe();
}
async addPubkey(userAccountPublicKey, userAccount, slot, accountSubscription) {
var _a;
const user = new user_1.User({
driftClient: this.driftClient,
userAccountPublicKey,
accountSubscription: accountSubscription !== null && accountSubscription !== void 0 ? accountSubscription : {
type: 'custom',
// OneShotUserAccountSubscriber used here so we don't load up the RPC with AccountSubscribes
userAccountSubscriber: new oneShotUserAccountSubscriber_1.OneShotUserAccountSubscriber(this.driftClient.program, userAccountPublicKey, userAccount, slot, this.commitment),
},
});
await user.subscribe(userAccount);
this.userMap.set(userAccountPublicKey.toString(), {
data: user,
slot: slot !== null && slot !== void 0 ? slot : (_a = user.getUserAccountAndSlot()) === null || _a === void 0 ? void 0 : _a.slot,
});
this.eventEmitter.emit('userUpdate', user);
}
has(key) {
return this.userMap.has(key);
}
/**
* gets the User for a particular userAccountPublicKey, if no User exists, undefined is returned
* @param key userAccountPublicKey to get User for
* @returns user User | undefined
*/
get(key) {
var _a;
return (_a = this.userMap.get(key)) === null || _a === void 0 ? void 0 : _a.data;
}
getWithSlot(key) {
return this.userMap.get(key);
}
/**
* gets the User for a particular userAccountPublicKey, if no User exists, new one is created
* @param key userAccountPublicKey to get User for
* @returns User
*/
async mustGet(key, accountSubscription) {
if (!this.has(key)) {
await this.addPubkey(new web3_js_1.PublicKey(key), undefined, undefined, accountSubscription);
}
return this.userMap.get(key).data;
}
async mustGetWithSlot(key, accountSubscription) {
if (!this.has(key)) {
await this.addPubkey(new web3_js_1.PublicKey(key), undefined, undefined, accountSubscription);
}
return this.userMap.get(key);
}
async mustGetUserAccount(key) {
const user = await this.mustGet(key);
return user.getUserAccount();
}
/**
* gets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned
* @param key userAccountPublicKey to get User for
* @returns authority PublicKey | undefined
*/
getUserAuthority(key) {
const user = this.userMap.get(key);
if (!user) {
return undefined;
}
return user.data.getUserAccount().authority;
}
/**
* implements the {@link DLOBSource} interface
* create a DLOB from all the subscribed users
* @param slot
*/
async getDLOB(slot, protectedMakerParamsMap) {
const dlob = new DLOB_1.DLOB(protectedMakerParamsMap);
await dlob.initFromUserMap(this, slot);
return dlob;
}
async updateWithOrderRecord(record) {
if (!this.has(record.user.toString())) {
await this.addPubkey(record.user);
}
}
async updateWithEventRecord(record) {
if (record.eventType === 'DepositRecord') {
const depositRecord = record;
await this.mustGet(depositRecord.user.toString());
}
else if (record.eventType === 'FundingPaymentRecord') {
const fundingPaymentRecord = record;
await this.mustGet(fundingPaymentRecord.user.toString());
}
else if (record.eventType === 'LiquidationRecord') {
const liqRecord = record;
await this.mustGet(liqRecord.user.toString());
await this.mustGet(liqRecord.liquidator.toString());
}
else if (record.eventType === 'OrderRecord') {
const orderRecord = record;
await this.updateWithOrderRecord(orderRecord);
}
else if (record.eventType === 'OrderActionRecord') {
const actionRecord = record;
if (actionRecord.taker) {
await this.mustGet(actionRecord.taker.toString());
}
if (actionRecord.maker) {
await this.mustGet(actionRecord.maker.toString());
}
}
else if (record.eventType === 'SettlePnlRecord') {
const settlePnlRecord = record;
await this.mustGet(settlePnlRecord.user.toString());
}
else if (record.eventType === 'NewUserRecord') {
const newUserRecord = record;
await this.mustGet(newUserRecord.user.toString());
}
else if (record.eventType === 'LPRecord') {
const lpRecord = record;
await this.mustGet(lpRecord.user.toString());
}
}
*values() {
for (const dataAndSlot of this.userMap.values()) {
yield dataAndSlot.data;
}
}
valuesWithSlot() {
return this.userMap.values();
}
*entries() {
for (const [key, dataAndSlot] of this.userMap.entries()) {
yield [key, dataAndSlot.data];
}
}
entriesWithSlot() {
return this.userMap.entries();
}
size() {
return this.userMap.size;
}
/**
* Returns a unique list of authorities for all users in the UserMap that meet the filter criteria
* @param filterCriteria: Users must meet these criteria to be included
* @returns
*/
getUniqueAuthorities(filterCriteria) {
const usersMeetingCriteria = Array.from(this.values()).filter((user) => {
let pass = true;
if (filterCriteria && filterCriteria.hasOpenOrders) {
pass = pass && user.getUserAccount().hasOpenOrder;
}
return pass;
});
const userAuths = new Set(usersMeetingCriteria.map((user) => user.getUserAccount().authority.toBase58()));
const userAuthKeys = Array.from(userAuths).map((userAuth) => new web3_js_1.PublicKey(userAuth));
return userAuthKeys;
}
async sync() {
if (this.syncConfig.type === 'default') {
return this.defaultSync();
}
else {
return this.paginatedSync();
}
}
getFilters() {
const filters = [(0, memcmp_1.getUserFilter)()];
if (!this.includeIdle) {
filters.push((0, memcmp_1.getNonIdleUserFilter)());
}
if (this.filterByPoolId !== undefined) {
filters.push((0, memcmp_1.getUsersWithPoolId)(this.filterByPoolId));
}
if (this.additionalFilters) {
filters.push(...this.additionalFilters);
}
return filters;
}
/**
* Syncs the UserMap using the default sync method (single getProgramAccounts call with filters).
* This method may fail when drift has too many users. (nodejs response size limits)
* @returns
*/
async defaultSync() {
var _a;
if (this.syncPromise) {
return this.syncPromise;
}
this.syncPromise = new Promise((resolver) => {
this.syncPromiseResolver = resolver;
});
try {
const rpcRequestArgs = [
this.driftClient.program.programId.toBase58(),
{
commitment: this.commitment,
filters: this.getFilters(),
encoding: 'base64+zstd',
withContext: true,
},
];
// @ts-ignore
const rpcJSONResponse = await this.connection._rpcRequest('getProgramAccounts', rpcRequestArgs);
const rpcResponseAndContext = rpcJSONResponse.result;
const slot = rpcResponseAndContext.context.slot;
this.updateLatestSlot(slot);
const programAccountBufferMap = new Map();
const decodingPromises = rpcResponseAndContext.value.map(async (programAccount) => {
const compressedUserData = buffer_1.Buffer.from(programAccount.account.data[0], 'base64');
const decoder = new zstddec_1.ZSTDDecoder();
await decoder.init();
const userBuffer = decoder.decode(compressedUserData, MAX_USER_ACCOUNT_SIZE_BYTES);
programAccountBufferMap.set(programAccount.pubkey.toString(), buffer_1.Buffer.from(userBuffer));
});
await Promise.all(decodingPromises);
const promises = Array.from(programAccountBufferMap.entries()).map(([key, buffer]) => (async () => {
const currAccountWithSlot = this.getWithSlot(key);
if (currAccountWithSlot) {
if (slot >= currAccountWithSlot.slot) {
const userAccount = this.decode('User', buffer);
this.updateUserAccount(key, userAccount, slot);
}
}
else {
const userAccount = this.decode('User', buffer);
await this.addPubkey(new web3_js_1.PublicKey(key), userAccount, slot);
}
})());
await Promise.all(promises);
for (const [key] of this.entries()) {
if (!programAccountBufferMap.has(key)) {
const user = this.get(key);
if (user) {
await user.unsubscribe();
this.userMap.delete(key);
}
}
}
}
catch (err) {
const e = err;
console.error(`Error in UserMap.sync(): ${e.message} ${(_a = e.stack) !== null && _a !== void 0 ? _a : ''}`);
if (this.throwOnFailedSync) {
throw e;
}
}
finally {
this.syncPromiseResolver();
this.syncPromise = undefined;
}
}
/**
* Syncs the UserMap using the paginated sync method (multiple getMultipleAccounts calls with filters).
* This method is more reliable when drift has many users.
* @returns
*/
async paginatedSync() {
var _a, _b;
if (this.syncPromise) {
return this.syncPromise;
}
this.syncPromise = new Promise((resolve) => {
this.syncPromiseResolver = resolve;
});
try {
const accountsPrefetch = await this.connection.getProgramAccounts(this.driftClient.program.programId, {
dataSlice: { offset: 0, length: 0 },
filters: this.getFilters(),
});
const accountPublicKeys = 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 Map();
// @ts-ignore
const chunkSize = (_a = this.syncConfig.chunkSize) !== null && _a !== void 0 ? _a : 100;
const tasks = [];
for (let i = 0; i < accountPublicKeys.length; i += chunkSize) {
const chunk = accountPublicKeys.slice(i, i + chunkSize);
tasks.push(async () => {
const accountInfos = await this.connection.getMultipleAccountsInfoAndContext(chunk, {
commitment: this.commitment,
});
const accountInfosSlot = accountInfos.context.slot;
for (let j = 0; j < accountInfos.value.length; j += 1) {
const accountInfo = accountInfos.value[j];
if (accountInfo === null)
continue;
const publicKeyString = chunk[j].toString();
const buffer = buffer_1.Buffer.from(accountInfo.data);
programAccountBufferMap.set(publicKeyString, buffer);
const decodedUser = this.decode('User', buffer);
const currAccountWithSlot = this.getWithSlot(publicKeyString);
if (currAccountWithSlot &&
currAccountWithSlot.slot <= accountInfosSlot) {
this.updateUserAccount(publicKeyString, decodedUser, accountInfosSlot);
}
else {
await this.addPubkey(new web3_js_1.PublicKey(publicKeyString), decodedUser, accountInfosSlot);
}
}
});
}
// @ts-ignore
const concurrencyLimit = (_b = this.syncConfig.concurrencyLimit) !== null && _b !== void 0 ? _b : 10;
await limitConcurrency(tasks, concurrencyLimit);
for (const [key] of this.entries()) {
if (!programAccountBufferMap.has(key)) {
const user = this.get(key);
if (user) {
await user.unsubscribe();
this.userMap.delete(key);
}
}
}
}
catch (err) {
console.error(`Error in UserMap.sync():`, err);
if (this.throwOnFailedSync) {
throw err;
}
}
finally {
if (this.syncPromiseResolver) {
this.syncPromiseResolver();
}
this.syncPromise = undefined;
}
}
async unsubscribe() {
await this.subscription.unsubscribe();
for (const [key, user] of this.entries()) {
await user.unsubscribe();
this.userMap.delete(key);
}
if (this.lastNumberOfSubAccounts) {
if (!this.disableSyncOnTotalAccountsChange) {
this.driftClient.eventEmitter.removeListener('stateAccountUpdate', this.stateAccountUpdateCallback);
}
this.lastNumberOfSubAccounts = undefined;
}
}
async updateUserAccount(key, userAccount, slot) {
const userWithSlot = this.getWithSlot(key);
this.updateLatestSlot(slot);
if (userWithSlot) {
if (slot >= userWithSlot.slot) {
userWithSlot.data.accountSubscriber.updateData(userAccount, slot);
this.userMap.set(key, {
data: userWithSlot.data,
slot,
});
this.eventEmitter.emit('userUpdate', userWithSlot.data);
}
}
else {
this.addPubkey(new web3_js_1.PublicKey(key), userAccount, slot);
}
}
updateLatestSlot(slot) {
this.mostRecentSlot = Math.max(slot, this.mostRecentSlot);
}
getSlot() {
return this.mostRecentSlot;
}
}
exports.UserMap = UserMap;