@drift-labs/sdk
Version:
SDK for Drift Protocol
454 lines (453 loc) • 20.6 kB
JavaScript
"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;