@drift-labs/sdk
Version: 
SDK for Drift Protocol
127 lines (110 loc) • 3.42 kB
text/typescript
import {
	BlockhashWithExpiryBlockHeight,
	Commitment,
	Connection,
	Context,
} from '@solana/web3.js';
import { BlockhashSubscriberConfig } from './types';
export class BlockhashSubscriber {
	private connection: Connection;
	private isSubscribed = false;
	private latestBlockHeight: number;
	private latestBlockHeightContext: Context | undefined;
	private blockhashes: Array<BlockhashWithExpiryBlockHeight> = [];
	private updateBlockhashIntervalId: ReturnType<typeof setTimeout> | undefined;
	private commitment: Commitment;
	private updateIntervalMs: number;
	constructor(config: BlockhashSubscriberConfig) {
		if (!config.connection && !config.rpcUrl) {
			throw new Error(
				'BlockhashSubscriber requires one of connection or rpcUrl must be provided'
			);
		}
		this.connection = config.connection || new Connection(config.rpcUrl!);
		this.commitment = config.commitment ?? 'confirmed';
		this.updateIntervalMs = config.updateIntervalMs ?? 1000;
	}
	getBlockhashCacheSize(): number {
		return this.blockhashes.length;
	}
	getLatestBlockHeight(): number {
		return this.latestBlockHeight;
	}
	getLatestBlockHeightContext(): Context | undefined {
		return this.latestBlockHeightContext;
	}
	/**
	 * Returns the latest cached blockhash, based on an offset from the latest obtained
	 * @param offset Offset to use, defaulting to 0
	 * @param offsetType If 'seconds', it will use calculate the actual element offset based on the update interval; otherwise it will return a fixed index
	 * @returns Cached blockhash at the given offset, or undefined
	 */
	getLatestBlockhash(
		offset = 0,
		offsetType: 'index' | 'seconds' = 'index'
	): BlockhashWithExpiryBlockHeight | undefined {
		if (this.blockhashes.length === 0) {
			return undefined;
		}
		const elementOffset =
			offsetType == 'seconds'
				? Math.floor((offset * 1000) / this.updateIntervalMs)
				: offset;
		const clampedOffset = Math.max(
			0,
			Math.min(this.blockhashes.length - 1, elementOffset)
		);
		return this.blockhashes[this.blockhashes.length - 1 - clampedOffset];
	}
	pruneBlockhashes() {
		if (this.latestBlockHeight) {
			this.blockhashes = this.blockhashes.filter(
				(blockhash) => blockhash.lastValidBlockHeight > this.latestBlockHeight!
			);
		}
	}
	async updateBlockhash() {
		try {
			const [resp, lastConfirmedBlockHeight] = await Promise.all([
				this.connection.getLatestBlockhashAndContext({
					commitment: this.commitment,
				}),
				this.connection.getBlockHeight({ commitment: this.commitment }),
			]);
			this.latestBlockHeight = lastConfirmedBlockHeight;
			this.latestBlockHeightContext = resp.context;
			// avoid caching duplicate blockhashes
			if (this.blockhashes.length > 0) {
				if (
					resp.value.blockhash ===
					this.blockhashes[this.blockhashes.length - 1].blockhash
				) {
					return;
				}
			}
			this.blockhashes.push(resp.value);
		} catch (e) {
			console.error('Error updating blockhash:\n', e);
		} finally {
			this.pruneBlockhashes();
		}
	}
	async subscribe() {
		if (this.isSubscribed) {
			return;
		}
		this.isSubscribed = true;
		await this.updateBlockhash();
		this.updateBlockhashIntervalId = setInterval(
			this.updateBlockhash.bind(this),
			this.updateIntervalMs
		);
	}
	unsubscribe() {
		if (this.updateBlockhashIntervalId) {
			clearInterval(this.updateBlockhashIntervalId);
			this.updateBlockhashIntervalId = undefined;
		}
		this.isSubscribed = false;
	}
}