@drift-labs/sdk-browser
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;