UNPKG

@drift-labs/sdk

Version:
454 lines (453 loc) • 20.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketProgramAccountSubscriberV2 = void 0; const web3_js_1 = require("@solana/web3.js"); const gill_1 = require("gill"); const bs58_1 = __importDefault(require("bs58")); class WebSocketProgramAccountSubscriberV2 { constructor(subscriptionName, accountDiscriminator, program, decodeBufferFn, options = { filters: [], }, resubOpts, accountsToMonitor // Optional list of accounts to poll ) { var _a; this.bufferAndSlotMap = new Map(); this.isUnsubscribing = false; this.receivingData = false; // Polling logic for specific accounts this.accountsToMonitor = new Set(); this.pollingIntervalMs = 30000; // 30 seconds this.pollingTimeouts = new Map(); this.lastWsNotificationTime = new Map(); // Track last WS notification time per account this.accountsCurrentlyPolling = new Set(); // Track which accounts are being polled this.subscriptionName = subscriptionName; this.accountDiscriminator = accountDiscriminator; this.program = program; this.decodeBuffer = decodeBufferFn; 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.options = options; this.receivingData = false; // Initialize accounts to monitor if (accountsToMonitor) { accountsToMonitor.forEach((account) => { this.accountsToMonitor.add(account.toBase58()); }); } // 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 subscribe(onChange) { var _a, _b; if (this.listenerId != null || this.isUnsubscribing) { return; } this.onChange = onChange; // Create abort controller for proper cleanup const abortController = new AbortController(); this.abortController = abortController; // Subscribe to program account changes using gill's rpcSubscriptions const programId = this.program.programId.toBase58(); if ((0, gill_1.isAddress)(programId)) { const subscription = await this.rpcSubscriptions .programNotifications(programId, { commitment: this.options.commitment, encoding: 'base64', filters: this.options.filters.map((filter) => ({ memcmp: { offset: BigInt(filter.memcmp.offset), bytes: filter.memcmp.bytes, encoding: 'base64', }, })), }) .subscribe({ abortSignal: abortController.signal, }); 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.account); this.setTimeout(); } else { this.handleRpcResponse(notification.context, notification.value.account); } } } 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(); } // Start monitoring for accounts that may need polling if no WS event is received this.startMonitoringForAccounts(); } setTimeout() { var _a; if (!this.onChange) { throw new Error('onChange callback function must be set'); } this.timeoutId = setTimeout(async () => { var _a, _b; if (this.isUnsubscribing) { // If we are in the process of unsubscribing, do not attempt to resubscribe return; } if (this.receivingData) { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log(`No ws data from ${this.subscriptionName} in ${(_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.resubTimeoutMs}ms, resubscribing`); } await this.unsubscribe(true); this.receivingData = false; await this.subscribe(this.onChange); } }, (_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs); } handleRpcResponse(context, accountInfo) { const newSlot = Number(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') { // Convert base58 to buffer using bs58 newBuffer = Buffer.from(bs58_1.default.decode(data)); } else { newBuffer = Buffer.from(data, 'base64'); } } } } // Convert gill's account key to PublicKey // Note: accountInfo doesn't have a key property, we need to get it from the notification // For now, we'll use a placeholder - this needs to be fixed based on the actual gill API const accountId = new web3_js_1.PublicKey('11111111111111111111111111111111'); // Placeholder const accountIdString = accountId.toBase58(); const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); // Track WebSocket notification time for this account this.lastWsNotificationTime.set(accountIdString, Date.now()); // If this account was being polled, stop polling it if (this.accountsCurrentlyPolling.has(accountIdString)) { this.accountsCurrentlyPolling.delete(accountIdString); // If no more accounts are being polled, stop batch polling if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) { clearTimeout(this.batchPollingTimeout); this.batchPollingTimeout = undefined; } } if (!existingBufferAndSlot) { if (newBuffer) { this.bufferAndSlotMap.set(accountIdString, { buffer: newBuffer, slot: newSlot, }); const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); this.onChange(accountId, account, { slot: newSlot }, newBuffer); } return; } if (newSlot < existingBufferAndSlot.slot) { return; } const oldBuffer = existingBufferAndSlot.buffer; if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { this.bufferAndSlotMap.set(accountIdString, { buffer: newBuffer, slot: newSlot, }); const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); this.onChange(accountId, account, { slot: newSlot }, newBuffer); } } startMonitoringForAccounts() { // Clear any existing polling timeouts this.clearPollingTimeouts(); // Start monitoring for each account in the accountsToMonitor set this.accountsToMonitor.forEach((accountIdString) => { this.startMonitoringForAccount(accountIdString); }); } startMonitoringForAccount(accountIdString) { // Clear existing timeout for this account const existingTimeout = this.pollingTimeouts.get(accountIdString); if (existingTimeout) { clearTimeout(existingTimeout); } // Set up monitoring timeout - only start polling if no WS notification in 30s const timeoutId = setTimeout(async () => { // Check if we've received a WS notification for this account recently const lastNotificationTime = this.lastWsNotificationTime.get(accountIdString); const currentTime = Date.now(); if (!lastNotificationTime || currentTime - lastNotificationTime >= this.pollingIntervalMs) { // No recent WS notification, start polling await this.pollAccount(accountIdString); // Schedule next poll this.startPollingForAccount(accountIdString); } else { // We received a WS notification recently, continue monitoring this.startMonitoringForAccount(accountIdString); } }, this.pollingIntervalMs); this.pollingTimeouts.set(accountIdString, timeoutId); } startPollingForAccount(accountIdString) { // Add account to polling set this.accountsCurrentlyPolling.add(accountIdString); // If this is the first account being polled, start batch polling if (this.accountsCurrentlyPolling.size === 1) { this.startBatchPolling(); } } startBatchPolling() { // Clear existing batch polling timeout if (this.batchPollingTimeout) { clearTimeout(this.batchPollingTimeout); } // Set up batch polling interval this.batchPollingTimeout = setTimeout(async () => { await this.pollAllAccounts(); // Schedule next batch poll this.startBatchPolling(); }, this.pollingIntervalMs); } async pollAllAccounts() { var _a, _b; try { // Get all accounts currently being polled const accountsToPoll = Array.from(this.accountsCurrentlyPolling); if (accountsToPoll.length === 0) { return; } // Fetch all accounts in a single batch request const accountAddresses = accountsToPoll.map((accountId) => accountId); const rpcResponse = await this.rpc .getMultipleAccounts(accountAddresses, { commitment: this.options.commitment, encoding: 'base64', }) .send(); const currentSlot = Number(rpcResponse.context.slot); // Process each account response for (let i = 0; i < accountsToPoll.length; i++) { const accountIdString = accountsToPoll[i]; const accountInfo = rpcResponse.value[i]; if (!accountInfo) { continue; } const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); if (!existingBufferAndSlot) { // Account not in our map yet, add it let newBuffer = undefined; if (accountInfo.data) { if (Array.isArray(accountInfo.data)) { const [data, encoding] = accountInfo.data; newBuffer = Buffer.from(data, encoding); } } if (newBuffer) { this.bufferAndSlotMap.set(accountIdString, { buffer: newBuffer, slot: currentSlot, }); const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); const accountId = new web3_js_1.PublicKey(accountIdString); this.onChange(accountId, account, { slot: currentSlot }, newBuffer); } continue; } // Check if we missed an update if (currentSlot > existingBufferAndSlot.slot) { let newBuffer = undefined; if (accountInfo.data) { if (Array.isArray(accountInfo.data)) { const [data, encoding] = accountInfo.data; if (encoding === 'base58') { newBuffer = Buffer.from(bs58_1.default.decode(data)); } else { newBuffer = Buffer.from(data, 'base64'); } } } // Check if buffer has changed if (newBuffer && (!existingBufferAndSlot.buffer || !newBuffer.equals(existingBufferAndSlot.buffer))) { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log(`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`); } // We missed an update, resubscribe await this.unsubscribe(true); this.receivingData = false; await this.subscribe(this.onChange); return; } } } } catch (error) { if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.logResubMessages) { console.log(`[${this.subscriptionName}] Error batch polling accounts:`, error); } } } async pollAccount(accountIdString) { var _a, _b; try { // Fetch current account data using gill's rpc const accountAddress = accountIdString; const rpcResponse = await this.rpc .getAccountInfo(accountAddress, { commitment: this.options.commitment, encoding: 'base64', }) .send(); const currentSlot = Number(rpcResponse.context.slot); const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); if (!existingBufferAndSlot) { // Account not in our map yet, add it if (rpcResponse.value) { let newBuffer = undefined; if (rpcResponse.value.data) { if (Array.isArray(rpcResponse.value.data)) { const [data, encoding] = rpcResponse.value.data; newBuffer = Buffer.from(data, encoding); } } if (newBuffer) { this.bufferAndSlotMap.set(accountIdString, { buffer: newBuffer, slot: currentSlot, }); const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); const accountId = new web3_js_1.PublicKey(accountIdString); this.onChange(accountId, account, { slot: currentSlot }, newBuffer); } } return; } // Check if we missed an update if (currentSlot > existingBufferAndSlot.slot) { let newBuffer = undefined; if (rpcResponse.value) { if (rpcResponse.value.data) { if (Array.isArray(rpcResponse.value.data)) { const [data, encoding] = rpcResponse.value.data; if (encoding === 'base58') { newBuffer = Buffer.from(bs58_1.default.decode(data)); } else { newBuffer = Buffer.from(data, 'base64'); } } } } // Check if buffer has changed if (newBuffer && (!existingBufferAndSlot.buffer || !newBuffer.equals(existingBufferAndSlot.buffer))) { if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) { console.log(`[${this.subscriptionName}] Polling detected missed update for account ${accountIdString}, resubscribing`); } // We missed an update, resubscribe await this.unsubscribe(true); this.receivingData = false; await this.subscribe(this.onChange); return; } } } catch (error) { if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.logResubMessages) { console.log(`[${this.subscriptionName}] Error polling account ${accountIdString}:`, error); } } } clearPollingTimeouts() { this.pollingTimeouts.forEach((timeoutId) => { clearTimeout(timeoutId); }); this.pollingTimeouts.clear(); // Clear batch polling timeout if (this.batchPollingTimeout) { clearTimeout(this.batchPollingTimeout); this.batchPollingTimeout = undefined; } // Clear accounts currently polling this.accountsCurrentlyPolling.clear(); } unsubscribe(onResub = false) { if (!onResub) { this.resubOpts.resubTimeoutMs = undefined; } this.isUnsubscribing = true; clearTimeout(this.timeoutId); this.timeoutId = undefined; // Clear polling timeouts this.clearPollingTimeouts(); // Abort the WebSocket subscription if (this.abortController) { this.abortController.abort('unsubscribing'); this.abortController = undefined; } this.listenerId = undefined; this.isUnsubscribing = false; return Promise.resolve(); } // Method to add accounts to the polling list addAccountToMonitor(accountId) { const accountIdString = accountId.toBase58(); this.accountsToMonitor.add(accountIdString); // If already subscribed, start monitoring for this account if (this.listenerId != null && !this.isUnsubscribing) { this.startMonitoringForAccount(accountIdString); } } // Method to remove accounts from the polling list removeAccountFromMonitor(accountId) { const accountIdString = accountId.toBase58(); this.accountsToMonitor.delete(accountIdString); // Clear monitoring timeout for this account const timeoutId = this.pollingTimeouts.get(accountIdString); if (timeoutId) { clearTimeout(timeoutId); this.pollingTimeouts.delete(accountIdString); } // Remove from currently polling set if it was being polled this.accountsCurrentlyPolling.delete(accountIdString); // If no more accounts are being polled, stop batch polling if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) { clearTimeout(this.batchPollingTimeout); this.batchPollingTimeout = undefined; } } // Method to set polling interval setPollingInterval(intervalMs) { this.pollingIntervalMs = intervalMs; // Restart monitoring with new interval if already subscribed if (this.listenerId != null && !this.isUnsubscribing) { this.startMonitoringForAccounts(); } } } exports.WebSocketProgramAccountSubscriberV2 = WebSocketProgramAccountSubscriberV2;