UNPKG

@drift-labs/sdk

Version:
224 lines (223 loc) • 9.52 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketAccountSubscriberV2 = void 0; const utils_1 = require("./utils"); const gill_1 = require("gill"); const bs58_1 = __importDefault(require("bs58")); class WebSocketAccountSubscriberV2 { constructor(accountName, program, accountPublicKey, decodeBuffer, resubOpts, commitment) { var _a; this.isUnsubscribing = false; this.accountName = accountName; this.logAccountName = `${accountName}-${accountPublicKey.toBase58()}-ws-acct-subscriber-v2`; this.program = program; this.accountPublicKey = accountPublicKey; this.decodeBufferFn = decodeBuffer; this.resubOpts = resubOpts; if (((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs) < 1000) { console.log(`resubTimeoutMs should be at least 1000ms to avoid spamming resub ${this.logAccountName}`); } this.receivingData = false; if (['recent', 'single', 'singleGossip', 'root', 'max'].includes(this.program.provider.opts.commitment)) { console.warn(`using commitment ${this.program.provider.opts.commitment} that is not supported by gill, this may cause issues`); } this.commitment = commitment !== null && commitment !== void 0 ? commitment : this.program.provider.opts.commitment; // Initialize gill client using the same RPC URL as the program provider const rpcUrl = this.program.provider.connection .rpcEndpoint; const { rpc, rpcSubscriptions } = (0, gill_1.createSolanaClient)({ urlOrMoniker: rpcUrl, }); this.rpc = rpc; this.rpcSubscriptions = rpcSubscriptions; } async handleNotificationLoop(subscription) { var _a; for await (const notification of subscription) { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs) { this.receivingData = true; clearTimeout(this.timeoutId); this.handleRpcResponse(notification.context, notification.value); this.setTimeout(); } else { this.handleRpcResponse(notification.context, notification.value); } } } async subscribe(onChange) { var _a, _b; if (this.listenerId != null || this.isUnsubscribing) { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log(`[${this.logAccountName}] Subscribe returning early - listenerId=${this.listenerId}, isUnsubscribing=${this.isUnsubscribing}`); } return; } this.onChange = onChange; if (!this.dataAndSlot) { await this.fetch(); } // Create abort controller for proper cleanup const abortController = new AbortController(); this.abortController = abortController; this.listenerId = Math.random(); // Unique ID for logging purposes if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.resubTimeoutMs) { this.receivingData = true; this.setTimeout(); } // Subscribe to account changes using gill's rpcSubscriptions const pubkey = this.accountPublicKey.toBase58(); if ((0, gill_1.isAddress)(pubkey)) { const subscription = await this.rpcSubscriptions .accountNotifications(pubkey, { commitment: this.commitment, encoding: 'base64', }) .subscribe({ abortSignal: abortController.signal, }); // Start notification loop without awaiting this.handleNotificationLoop(subscription); } } setData(data, slot) { const newSlot = slot || 0; if (this.dataAndSlot && this.dataAndSlot.slot > newSlot) { return; } this.dataAndSlot = { data, slot, }; } setTimeout() { var _a; if (!this.onChange) { throw new Error('onChange callback function must be set'); } this.timeoutId = setTimeout(async () => { var _a, _b, _c, _d; if (this.isUnsubscribing) { // If we are in the process of unsubscribing, do not attempt to resubscribe if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log(`[${this.logAccountName}] Timeout fired but isUnsubscribing=true, skipping resubscribe`); } return; } if (this.receivingData) { if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.logResubMessages) { console.log(`No ws data from ${this.logAccountName} in ${this.resubOpts.resubTimeoutMs}ms, resubscribing - listenerId=${this.listenerId}, isUnsubscribing=${this.isUnsubscribing}`); } await this.unsubscribe(true); this.receivingData = false; await this.subscribe(this.onChange); if ((_c = this.resubOpts) === null || _c === void 0 ? void 0 : _c.logResubMessages) { console.log(`[${this.logAccountName}] Resubscribe completed - receivingData=${this.receivingData}, listenerId=${this.listenerId}, isUnsubscribing=${this.isUnsubscribing}`); } } else { if ((_d = this.resubOpts) === null || _d === void 0 ? void 0 : _d.logResubMessages) { console.log(`[${this.logAccountName}] Timeout fired but receivingData=false, skipping resubscribe`); } } }, (_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs); } async fetch() { // Use gill's rpc for fetching account info const accountAddress = this.accountPublicKey.toBase58(); const rpcResponse = await this.rpc .getAccountInfo(accountAddress, { commitment: this.commitment, encoding: 'base64', }) .send(); // Convert gill response to match the expected format const context = { slot: Number(rpcResponse.context.slot), }; const accountInfo = rpcResponse.value; this.handleRpcResponse({ slot: BigInt(context.slot) }, accountInfo); } handleRpcResponse(context, accountInfo) { const newSlot = context.slot; let newBuffer = undefined; if (accountInfo) { // Extract data from gill response if (accountInfo.data) { // Handle different data formats from gill if (Array.isArray(accountInfo.data)) { // If it's a tuple [data, encoding] const [data, encoding] = accountInfo.data; if (encoding === 'base58') { // we know encoding will be base58 // Convert base58 to buffer using bs58 newBuffer = Buffer.from(bs58_1.default.decode(data)); } else { newBuffer = Buffer.from(data, 'base64'); } } } } if (!this.bufferAndSlot) { this.bufferAndSlot = { buffer: newBuffer, slot: Number(newSlot), }; if (newBuffer) { const account = this.decodeBuffer(newBuffer); this.dataAndSlot = { data: account, slot: Number(newSlot), }; this.onChange(account); } return; } if (Number(newSlot) < this.bufferAndSlot.slot) { return; } const oldBuffer = this.bufferAndSlot.buffer; if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { this.bufferAndSlot = { buffer: newBuffer, slot: Number(newSlot), }; const account = this.decodeBuffer(newBuffer); this.dataAndSlot = { data: account, slot: Number(newSlot), }; this.onChange(account); } } decodeBuffer(buffer) { if (this.decodeBufferFn) { return this.decodeBufferFn(buffer); } else { return this.program.account[this.accountName].coder.accounts.decode((0, utils_1.capitalize)(this.accountName), buffer); } } unsubscribe(onResub = false) { if (!onResub && this.resubOpts) { this.resubOpts.resubTimeoutMs = undefined; } this.isUnsubscribing = true; clearTimeout(this.timeoutId); this.timeoutId = undefined; // Abort the WebSocket subscription if (this.abortController) { this.abortController.abort('unsubscribing'); this.abortController = undefined; } this.listenerId = undefined; this.isUnsubscribing = false; return Promise.resolve(); } } exports.WebSocketAccountSubscriberV2 = WebSocketAccountSubscriberV2;