@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
248 lines (228 loc) • 6.37 kB
text/typescript
import { Connection, PublicKey, SYSVAR_CLOCK_PUBKEY } from '@solana/web3.js';
import { BulkAccountLoader } from '../accounts/bulkAccountLoader';
import {
Client,
deserializeClockData,
toNum,
Market,
getMarketLadder,
} from '@ellipsis-labs/phoenix-sdk';
import { PRICE_PRECISION } from '../constants/numericConstants';
import { BN } from '@coral-xyz/anchor';
import { L2Level, L2OrderBookGenerator } from '../dlob/orderBookLevels';
import { fastDecode } from '../decode/phoenix';
export type PhoenixMarketSubscriberConfig = {
connection: Connection;
programId: PublicKey;
marketAddress: PublicKey;
accountSubscription:
| {
// enables use to add web sockets in the future
type: 'polling';
accountLoader: BulkAccountLoader;
}
| {
type: 'websocket';
};
fastDecode?: boolean;
};
export class PhoenixSubscriber implements L2OrderBookGenerator {
connection: Connection;
client: Client;
programId: PublicKey;
marketAddress: PublicKey;
subscriptionType: 'polling' | 'websocket';
accountLoader: BulkAccountLoader | undefined;
market: Market;
marketCallbackId: string | number;
clockCallbackId: string | number;
// fastDecode omits trader data from the markets for faster decoding process
fastDecode: boolean;
subscribed: boolean;
lastSlot: number;
lastUnixTimestamp: number;
public constructor(config: PhoenixMarketSubscriberConfig) {
this.connection = config.connection;
this.programId = config.programId;
this.marketAddress = config.marketAddress;
if (config.accountSubscription.type === 'polling') {
this.subscriptionType = 'polling';
this.accountLoader = config.accountSubscription.accountLoader;
} else {
this.subscriptionType = 'websocket';
}
this.lastSlot = 0;
this.lastUnixTimestamp = 0;
this.fastDecode = config.fastDecode ?? true;
}
public async subscribe(): Promise<void> {
if (this.subscribed) {
return;
}
this.market = await Market.loadFromAddress({
connection: this.connection,
address: this.marketAddress,
});
const clock = deserializeClockData(
(await this.connection.getAccountInfo(SYSVAR_CLOCK_PUBKEY, 'confirmed'))
.data
);
this.lastUnixTimestamp = toNum(clock.unixTimestamp);
if (this.subscriptionType === 'websocket') {
this.marketCallbackId = this.connection.onAccountChange(
this.marketAddress,
(accountInfo, _ctx) => {
try {
if (this.fastDecode) {
this.market.data = fastDecode(accountInfo.data);
} else {
this.market = this.market.reload(accountInfo.data);
}
} catch {
console.error('Failed to reload Phoenix market data');
}
}
);
this.clockCallbackId = this.connection.onAccountChange(
SYSVAR_CLOCK_PUBKEY,
(accountInfo, ctx) => {
try {
this.lastSlot = ctx.slot;
const clock = deserializeClockData(accountInfo.data);
this.lastUnixTimestamp = toNum(clock.unixTimestamp);
} catch {
console.error('Failed to reload clock data');
}
}
);
} else {
this.marketCallbackId = await this.accountLoader.addAccount(
this.marketAddress,
(buffer, slot) => {
try {
this.lastSlot = slot;
if (buffer) {
if (this.fastDecode) {
this.market.data = fastDecode(buffer);
} else {
this.market = this.market.reload(buffer);
}
}
} catch {
console.error('Failed to reload Phoenix market data');
}
}
);
this.clockCallbackId = await this.accountLoader.addAccount(
SYSVAR_CLOCK_PUBKEY,
(buffer, slot) => {
try {
this.lastSlot = slot;
const clock = deserializeClockData(buffer);
this.lastUnixTimestamp = toNum(clock.unixTimestamp);
} catch {
console.error('Failed to reload clock data');
}
}
);
}
this.subscribed = true;
}
public getBestBid(): BN | undefined {
const ladder = getMarketLadder(
this.market,
this.lastSlot,
this.lastUnixTimestamp,
1
);
const bestBid = ladder.bids[0];
if (!bestBid) {
return undefined;
}
return this.convertPriceInTicksToPricePrecision(bestBid.priceInTicks);
}
public getBestAsk(): BN | undefined {
const ladder = getMarketLadder(
this.market,
this.lastSlot,
this.lastUnixTimestamp,
1
);
const bestAsk = ladder.asks[0];
if (!bestAsk) {
return undefined;
}
return this.convertPriceInTicksToPricePrecision(bestAsk.priceInTicks);
}
public convertPriceInTicksToPricePrecision(priceInTicks: BN): BN {
const quotePrecision = new BN(
Math.pow(10, this.market.data.header.quoteParams.decimals)
);
const denom = quotePrecision.muln(
this.market.data.header.rawBaseUnitsPerBaseUnit
);
const price = priceInTicks
.muln(this.market.data.quoteLotsPerBaseUnitPerTick)
.muln(toNum(this.market.data.header.quoteLotSize));
return price.mul(PRICE_PRECISION).div(denom);
}
public convertSizeInBaseLotsToMarketPrecision(sizeInBaseLots: BN): BN {
return sizeInBaseLots.muln(toNum(this.market.data.header.baseLotSize));
}
public getL2Bids(): Generator<L2Level> {
return this.getL2Levels('bids');
}
public getL2Asks(): Generator<L2Level> {
return this.getL2Levels('asks');
}
*getL2Levels(side: 'bids' | 'asks'): Generator<L2Level> {
const ladder = getMarketLadder(
this.market,
this.lastSlot,
this.lastUnixTimestamp,
20
);
for (let i = 0; i < ladder[side].length; i++) {
const { priceInTicks, sizeInBaseLots } = ladder[side][i];
try {
const size =
this.convertSizeInBaseLotsToMarketPrecision(sizeInBaseLots);
const updatedPrice =
this.convertPriceInTicksToPricePrecision(priceInTicks);
yield {
price: updatedPrice,
size,
sources: {
phoenix: size,
},
};
} catch {
continue;
}
}
}
public async unsubscribe(): Promise<void> {
if (!this.subscribed) {
return;
}
// remove listeners
if (this.subscriptionType === 'websocket') {
await this.connection.removeAccountChangeListener(
this.marketCallbackId as number
);
await this.connection.removeAccountChangeListener(
this.clockCallbackId as number
);
} else {
this.accountLoader.removeAccount(
this.marketAddress,
this.marketCallbackId as string
);
this.accountLoader.removeAccount(
SYSVAR_CLOCK_PUBKEY,
this.clockCallbackId as string
);
}
this.subscribed = false;
}
}