@raydium-io/raydium-sdk-v2
Version:
An SDK for building applications on top of Raydium.
431 lines (385 loc) • 13 kB
text/typescript
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import Decimal from "decimal.js";
import { ApiV3PoolInfoConcentratedItem } from "../../../api/type";
import { ClmmPoolInfo } from "../type";
import { MAX_TICK, MIN_TICK } from "./constants";
import { SqrtPriceMath, TickMath } from "./math";
import { getPdaTickArrayAddress } from "./pda";
import { TickQuery } from "./tickQuery";
import { TickArrayBitmapExtensionLayout } from "../layout";
export const TICK_ARRAY_SIZE = 60;
export const TICK_ARRAY_BITMAP_SIZE = 512;
export interface ReturnTypeGetTickPrice {
tick: number;
price: Decimal;
tickSqrtPriceX64: BN;
}
export interface ReturnTypeGetPriceAndTick {
tick: number;
price: Decimal;
}
export type Tick = {
tick: number;
liquidityNet: BN;
liquidityGross: BN;
feeGrowthOutsideX64A: BN;
feeGrowthOutsideX64B: BN;
rewardGrowthsOutsideX64: BN[];
};
export type TickArray = {
address: PublicKey;
poolId: PublicKey;
startTickIndex: number;
ticks: Tick[];
initializedTickCount: number;
};
export type TickState = {
tick: number;
liquidityNet: BN;
liquidityGross: BN;
feeGrowthOutsideX64A: BN;
feeGrowthOutsideX64B: BN;
tickCumulativeOutside: BN;
secondsPerLiquidityOutsideX64: BN;
secondsOutside: number;
rewardGrowthsOutside: BN[];
};
export type TickArrayState = {
ammPool: PublicKey;
startTickIndex: number;
ticks: TickState[];
initializedTickCount: number;
};
export class TickUtils {
public static getTickArrayAddressByTick(
programId: PublicKey,
poolId: PublicKey,
tickIndex: number,
tickSpacing: number,
): PublicKey {
const startIndex = TickUtils.getTickArrayStartIndexByTick(tickIndex, tickSpacing);
const { publicKey: tickArrayAddress } = getPdaTickArrayAddress(programId, poolId, startIndex);
return tickArrayAddress;
}
public static getTickOffsetInArray(tickIndex: number, tickSpacing: number): number {
if (tickIndex % tickSpacing != 0) {
throw new Error("tickIndex % tickSpacing not equal 0");
}
const startTickIndex = TickUtils.getTickArrayStartIndexByTick(tickIndex, tickSpacing);
const offsetInArray = Math.floor((tickIndex - startTickIndex) / tickSpacing);
if (offsetInArray < 0 || offsetInArray >= TICK_ARRAY_SIZE) {
throw new Error("tick offset in array overflow");
}
return offsetInArray;
}
public static getTickArrayBitIndex(tickIndex: number, tickSpacing: number): number {
const ticksInArray = TickQuery.tickCount(tickSpacing);
let startIndex: number = tickIndex / ticksInArray;
if (tickIndex < 0 && tickIndex % ticksInArray != 0) {
startIndex = Math.ceil(startIndex) - 1;
} else {
startIndex = Math.floor(startIndex);
}
return startIndex;
}
public static getTickArrayStartIndexByTick(tickIndex: number, tickSpacing: number): number {
return this.getTickArrayBitIndex(tickIndex, tickSpacing) * TickQuery.tickCount(tickSpacing);
}
public static getTickArrayOffsetInBitmapByTick(tick: number, tickSpacing: number): number {
const multiplier = tickSpacing * TICK_ARRAY_SIZE;
const compressed = Math.floor(tick / multiplier) + 512;
return Math.abs(compressed);
}
public static checkTickArrayIsInitialized(
bitmap: BN,
tick: number,
tickSpacing: number,
): {
isInitialized: boolean;
startIndex: number;
} {
const multiplier = tickSpacing * TICK_ARRAY_SIZE;
const compressed = Math.floor(tick / multiplier) + 512;
const bitPos = Math.abs(compressed);
return {
isInitialized: bitmap.testn(bitPos),
startIndex: (bitPos - 512) * multiplier,
};
}
public static getNextTickArrayStartIndex(
lastTickArrayStartIndex: number,
tickSpacing: number,
zeroForOne: boolean,
): number {
return zeroForOne
? lastTickArrayStartIndex - tickSpacing * TICK_ARRAY_SIZE
: lastTickArrayStartIndex + tickSpacing * TICK_ARRAY_SIZE;
}
public static mergeTickArrayBitmap(bns: BN[]): BN {
let b = new BN(0);
for (let i = 0; i < bns.length; i++) {
b = b.add(bns[i].shln(64 * i));
}
return b;
}
public static getInitializedTickArrayInRange(
tickArrayBitmap: BN[],
exTickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>,
tickSpacing: number,
tickArrayStartIndex: number,
expectedCount: number,
): number[] {
const tickArrayOffset = Math.floor(tickArrayStartIndex / (tickSpacing * TICK_ARRAY_SIZE));
return [
// find right of currenct offset
...TickUtils.searchLowBitFromStart(
tickArrayBitmap,
exTickArrayBitmap,
tickArrayOffset - 1,
expectedCount,
tickSpacing,
),
// find left of current offset
...TickUtils.searchHightBitFromStart(
tickArrayBitmap,
exTickArrayBitmap,
tickArrayOffset,
expectedCount,
tickSpacing,
),
];
}
public static getAllInitializedTickArrayStartIndex(
tickArrayBitmap: BN[],
exTickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>,
tickSpacing: number,
): number[] {
// find from offset 0 to 1024
return TickUtils.searchHightBitFromStart(
tickArrayBitmap,
exTickArrayBitmap,
-7680,
TICK_ARRAY_BITMAP_SIZE,
tickSpacing,
);
}
public static getAllInitializedTickArrayInfo(
programId: PublicKey,
poolId: PublicKey,
tickArrayBitmap: BN[],
exTickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>,
tickSpacing: number,
): {
tickArrayStartIndex: number;
tickArrayAddress: PublicKey;
}[] {
const result: {
tickArrayStartIndex: number;
tickArrayAddress: PublicKey;
}[] = [];
const allInitializedTickArrayIndex: number[] = TickUtils.getAllInitializedTickArrayStartIndex(
tickArrayBitmap,
exTickArrayBitmap,
tickSpacing,
);
for (const startIndex of allInitializedTickArrayIndex) {
const { publicKey: address } = getPdaTickArrayAddress(programId, poolId, startIndex);
result.push({
tickArrayStartIndex: startIndex,
tickArrayAddress: address,
});
}
return result;
}
public static getAllInitializedTickInTickArray(tickArray: TickArrayState): TickState[] {
return tickArray.ticks.filter((i) => i.liquidityGross.gtn(0));
}
public static searchLowBitFromStart(
tickArrayBitmap: BN[],
exTickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>,
currentTickArrayBitStartIndex: number,
expectedCount: number,
tickSpacing: number,
): number[] {
const tickArrayBitmaps = [
...[...exTickArrayBitmap.negativeTickArrayBitmap].reverse(),
tickArrayBitmap.slice(0, 8),
tickArrayBitmap.slice(8, 16),
...exTickArrayBitmap.positiveTickArrayBitmap,
].map((i) => TickUtils.mergeTickArrayBitmap(i));
const result: number[] = [];
while (currentTickArrayBitStartIndex >= -7680) {
const arrayIndex = Math.floor((currentTickArrayBitStartIndex + 7680) / 512);
const searchIndex = (currentTickArrayBitStartIndex + 7680) % 512;
if (tickArrayBitmaps[arrayIndex].testn(searchIndex)) result.push(currentTickArrayBitStartIndex);
currentTickArrayBitStartIndex--;
if (result.length === expectedCount) break;
}
const tickCount = TickQuery.tickCount(tickSpacing);
return result.map((i) => i * tickCount);
}
public static searchHightBitFromStart(
tickArrayBitmap: BN[],
exTickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>,
currentTickArrayBitStartIndex: number,
expectedCount: number,
tickSpacing: number,
): number[] {
const tickArrayBitmaps = [
...[...exTickArrayBitmap.negativeTickArrayBitmap].reverse(),
tickArrayBitmap.slice(0, 8),
tickArrayBitmap.slice(8, 16),
...exTickArrayBitmap.positiveTickArrayBitmap,
].map((i) => TickUtils.mergeTickArrayBitmap(i));
const result: number[] = [];
while (currentTickArrayBitStartIndex < 7680) {
const arrayIndex = Math.floor((currentTickArrayBitStartIndex + 7680) / 512);
const searchIndex = (currentTickArrayBitStartIndex + 7680) % 512;
if (tickArrayBitmaps[arrayIndex].testn(searchIndex)) result.push(currentTickArrayBitStartIndex);
currentTickArrayBitStartIndex++;
if (result.length === expectedCount) break;
}
const tickCount = TickQuery.tickCount(tickSpacing);
return result.map((i) => i * tickCount);
}
public static checkIsOutOfBoundary(tick: number): boolean {
return tick < MIN_TICK || tick > MAX_TICK;
}
public static nextInitTick(
tickArrayCurrent: TickArray,
currentTickIndex: number,
tickSpacing: number,
zeroForOne: boolean,
t: boolean,
): Tick | null {
const currentTickArrayStartIndex = TickQuery.getArrayStartIndex(currentTickIndex, tickSpacing);
if (currentTickArrayStartIndex != tickArrayCurrent.startTickIndex) {
return null;
}
let offsetInArray = Math.floor((currentTickIndex - tickArrayCurrent.startTickIndex) / tickSpacing);
if (zeroForOne) {
while (offsetInArray >= 0) {
if (tickArrayCurrent.ticks[offsetInArray].liquidityGross.gtn(0)) {
return tickArrayCurrent.ticks[offsetInArray];
}
offsetInArray = offsetInArray - 1;
}
} else {
if (!t) offsetInArray = offsetInArray + 1;
while (offsetInArray < TICK_ARRAY_SIZE) {
if (tickArrayCurrent.ticks[offsetInArray].liquidityGross.gtn(0)) {
return tickArrayCurrent.ticks[offsetInArray];
}
offsetInArray = offsetInArray + 1;
}
}
return null;
}
public static firstInitializedTick(tickArrayCurrent: TickArray, zeroForOne: boolean): Tick {
if (zeroForOne) {
let i = TICK_ARRAY_SIZE - 1;
while (i >= 0) {
if (tickArrayCurrent.ticks[i].liquidityGross.gtn(0)) {
return tickArrayCurrent.ticks[i];
}
i = i - 1;
}
} else {
let i = 0;
while (i < TICK_ARRAY_SIZE) {
if (tickArrayCurrent.ticks[i].liquidityGross.gtn(0)) {
return tickArrayCurrent.ticks[i];
}
i = i + 1;
}
}
throw Error(`firstInitializedTick check error: ${tickArrayCurrent} - ${zeroForOne}`);
}
public static _getTickPriceLegacy({
poolInfo,
tick,
baseIn,
}: {
poolInfo: ClmmPoolInfo;
tick: number;
baseIn: boolean;
}): ReturnTypeGetTickPrice {
const tickSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(tick);
const tickPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
tickSqrtPriceX64,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
return baseIn
? { tick, price: tickPrice, tickSqrtPriceX64 }
: { tick, price: new Decimal(1).div(tickPrice), tickSqrtPriceX64 };
}
public static _getPriceAndTickLegacy({
poolInfo,
price,
baseIn,
}: {
poolInfo: ClmmPoolInfo;
price: Decimal;
baseIn: boolean;
}): ReturnTypeGetPriceAndTick {
const _price = baseIn ? price : new Decimal(1).div(price);
const tick = TickMath.getTickWithPriceAndTickspacing(
_price,
poolInfo.ammConfig.tickSpacing,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const tickSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(tick);
const tickPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
tickSqrtPriceX64,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
return baseIn ? { tick, price: tickPrice } : { tick, price: new Decimal(1).div(tickPrice) };
}
public static getTickPrice({
poolInfo,
tick,
baseIn,
}: {
poolInfo: ApiV3PoolInfoConcentratedItem;
tick: number;
baseIn: boolean;
}): ReturnTypeGetTickPrice {
const tickSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(tick);
const tickPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
tickSqrtPriceX64,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
return baseIn
? { tick, price: tickPrice, tickSqrtPriceX64 }
: { tick, price: new Decimal(1).div(tickPrice), tickSqrtPriceX64 };
}
public static getPriceAndTick({
poolInfo,
price,
baseIn,
}: {
poolInfo: ApiV3PoolInfoConcentratedItem;
price: Decimal;
baseIn: boolean;
}): ReturnTypeGetPriceAndTick {
const _price = baseIn ? price : new Decimal(1).div(price);
const tick = TickMath.getTickWithPriceAndTickspacing(
_price,
poolInfo.config.tickSpacing,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const tickSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(tick);
const tickPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
tickSqrtPriceX64,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
return baseIn ? { tick, price: tickPrice } : { tick, price: new Decimal(1).div(tickPrice) };
}
}