UNPKG

@raydium-io/raydium-sdk-v2

Version:

An SDK for building applications on top of Raydium.

689 lines (588 loc) 22.4 kB
import { FEE_RATE_DENOMINATOR_VALUE, getMultipleAccountsInfo } from "@/common"; import { Connection, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import Decimal from "decimal.js"; import { PoolInfoLayout, TickArrayBitmapExtensionLayout, TickArrayLayout, TickLayout } from "../layout"; import { mostSignificantBit, mulDivCeil, mulDivFloor } from "./bigNum"; import { BIT_PRECISION, BN_ONE, BN_ZERO, EXTENSION_TICKARRAY_BITMAP_SIZE, LOG_B_2_X32, LOG_B_P_ERR_MARGIN_LOWER_X64, LOG_B_P_ERR_MARGIN_UPPER_X64, MAX_SQRT_PRICE_X64, MAX_TICK, MIN_SQRT_PRICE_X64, MIN_TICK, Q64, TICK_ARRAY_BITMAP_SIZE, TICK_ARRAY_SIZE, TICK_TO_SQRT_PRICE_FACTORS, } from "./constants"; import { getPdaExBitmapAccount, getPdaTickArrayAddress } from "./pda"; export interface LimitOrderMatchResult { amountIn: BN; amountOut: BN; ammFeeAmount: BN; } export class TickArrayBitmapUtil { private static scanLinearBitmap({ bitmap, tickSpacing, offset, checkInfo, }: { bitmap: Buffer; tickSpacing: number; offset: number; checkInfo?: { tick: number; valueType: "lte" | "gte" }; }): number[] { const result: number[] = []; const totalBits = bitmap.length * 8; let startBit = 0; let endBit = totalBits - 1; if (checkInfo) { const threshold = checkInfo.tick / (tickSpacing * TICK_ARRAY_SIZE) - offset; if (checkInfo.valueType === "gte") { startBit = Math.max(0, Math.ceil(threshold)); } else { endBit = Math.min(totalBits - 1, Math.floor(threshold)); } } if (startBit > endBit) return result; const startByte = Math.floor(startBit / 8); const endByte = Math.floor(endBit / 8); for (let i = startByte; i <= endByte; i++) { if (!bitmap[i]) continue; const jStart = i === startByte ? startBit % 8 : 0; const jEnd = i === endByte ? endBit % 8 : 7; for (let j = jStart; j <= jEnd; j++) { if (bitmap[i] & (1 << j)) { result.push((i * 8 + j + offset) * tickSpacing * TICK_ARRAY_SIZE); } } } return result; } private static findPoolBitmap({ bitmap, tickSpacing, checkInfo, }: { bitmap: Buffer; tickSpacing: number; checkInfo?: { tick: number; valueType: "lte" | "gte" }; }): number[] { if (checkInfo) { const _i = Math.floor(checkInfo.tick / TICK_ARRAY_SIZE / tickSpacing); if (checkInfo.valueType === "lte" && _i < -512) return []; if (checkInfo.valueType === "gte" && _i > 512) return []; } return this.scanLinearBitmap({ bitmap, tickSpacing, offset: -TICK_ARRAY_BITMAP_SIZE, checkInfo }); } private static findPositiveTickArrayBitmap({ bitmap, tickSpacing, checkInfo, }: { bitmap: Buffer; tickSpacing: number; checkInfo?: { tick: number; valueType: "lte" | "gte" }; }): number[] { if (checkInfo) { const _i = Math.floor(checkInfo.tick / TICK_ARRAY_SIZE / tickSpacing); if (checkInfo.valueType === "lte" && _i < 512) return []; } return this.scanLinearBitmap({ bitmap, tickSpacing, offset: TICK_ARRAY_BITMAP_SIZE, checkInfo }); } private static findNegativeTickArrayBitmap({ bitmap, tickSpacing, count, checkInfo, }: { bitmap: Buffer; tickSpacing: number; count?: number; checkInfo?: { tick: number; valueType: "lte" | "gte" }; }): number[] { const result: number[] = []; if (checkInfo) { const _i = Math.floor(checkInfo.tick / TICK_ARRAY_SIZE / tickSpacing); if (checkInfo.valueType === "gte" && _i >= -512) return result; } const maxFlatIndex = checkInfo?.valueType === "lte" ? Math.floor(checkInfo.tick / (TICK_ARRAY_SIZE * tickSpacing)) + 7680 : Infinity; const minFlatIndex = checkInfo?.valueType === "gte" ? Math.ceil(checkInfo.tick / (TICK_ARRAY_SIZE * tickSpacing)) + 7680 : 0; outer: for (let arrayIndex = 0; arrayIndex < EXTENSION_TICKARRAY_BITMAP_SIZE; arrayIndex++) { const reversedIndex = EXTENSION_TICKARRAY_BITMAP_SIZE - 1 - arrayIndex; for (let searchIndex = 0; searchIndex < 512; searchIndex++) { const flatIndex = arrayIndex * 512 + searchIndex; if (flatIndex > maxFlatIndex) break outer; if (flatIndex < minFlatIndex) continue; const byteOffset = reversedIndex * 64 + Math.floor(searchIndex / 8); if (!bitmap[byteOffset]) { searchIndex = Math.floor(searchIndex / 8) * 8 + 7; continue; } if (bitmap[byteOffset] & (1 << searchIndex % 8)) { const tick = (arrayIndex * 512 + searchIndex - 7680) * TICK_ARRAY_SIZE * tickSpacing; result.push(tick); if (count !== undefined && result.length >= count) break outer; } } } return result; } static findTickArrayStartIndex({ tickSpacing, poolBitmap, tickArrayBitmap, findInfo, }: { tickSpacing: number; poolBitmap: ReturnType<typeof PoolInfoLayout.decode>["tickArrayBitmap"]; tickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>; findInfo: { type: "zeroForOne" | "oneForZero"; count?: number; tickArrayCurrent: number } | { type: "all" }; }): number[] { if (findInfo.type === "all") { return [ ...this.findNegativeTickArrayBitmap({ tickSpacing, bitmap: tickArrayBitmap.negativeTickArrayBitmap }), ...this.findPoolBitmap({ tickSpacing, bitmap: poolBitmap }), ...this.findPositiveTickArrayBitmap({ tickSpacing, bitmap: tickArrayBitmap.positiveTickArrayBitmap }), ]; } const tickStart = TickArrayUtil.getTickArrayStartIndex(findInfo.tickArrayCurrent, tickSpacing); const { count } = findInfo; if (findInfo.type === "oneForZero") { const checkInfo = { tick: tickStart, valueType: "gte" } as const; const finders = [ () => this.findNegativeTickArrayBitmap({ tickSpacing, bitmap: tickArrayBitmap.negativeTickArrayBitmap, checkInfo }), () => this.findPoolBitmap({ tickSpacing, bitmap: poolBitmap, checkInfo }), () => this.findPositiveTickArrayBitmap({ tickSpacing, bitmap: tickArrayBitmap.positiveTickArrayBitmap, checkInfo }), ]; return this.collectUntil(finders, count); } if (findInfo.type === "zeroForOne") { const checkInfo = { tick: tickStart, valueType: "lte" } as const; const finders = [ () => this.findPositiveTickArrayBitmap({ tickSpacing, bitmap: tickArrayBitmap.positiveTickArrayBitmap, checkInfo, }).sort((a, b) => b - a), () => this.findPoolBitmap({ tickSpacing, bitmap: poolBitmap, checkInfo }).sort((a, b) => b - a), () => this.findNegativeTickArrayBitmap({ tickSpacing, bitmap: tickArrayBitmap.negativeTickArrayBitmap, checkInfo, }).sort((a, b) => b - a), ]; return this.collectUntil(finders, count); } throw new Error("find info type check error"); } private static collectUntil(finders: Array<() => number[]>, count: number | undefined): number[] { const collected: number[] = []; for (const finder of finders) { if (count !== undefined && collected.length >= count) break; collected.push(...finder()); } return collected.slice(0, count); } static findTickArrayAddress(params: { programId: PublicKey; poolId: PublicKey; tickSpacing: number; poolBitmap: ReturnType<typeof PoolInfoLayout.decode>["tickArrayBitmap"]; tickArrayBitmap: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>; findInfo: { type: "zeroForOne" | "oneForZero"; count?: number; tickArrayCurrent: number } | { type: "all" }; }) { return this.findTickArrayStartIndex(params).map( (i) => getPdaTickArrayAddress(params.programId, params.poolId, i).publicKey, ); } static maxTickInTickarrayBitmap(tickSpacing: number): number { return tickSpacing * TICK_ARRAY_SIZE * TICK_ARRAY_BITMAP_SIZE; } } export class TickArrayUtil { static firstinitializedTick({ data, zeroForOne, }: { data: ReturnType<typeof TickArrayLayout.decode>; zeroForOne: boolean; }) { if (zeroForOne) { for (let i = data.ticks.length - 1; i >= 0; i--) { if (TickUtil.isInitialized({ data: data.ticks[i] })) return data.ticks[i]; } } else { for (let i = 0; i < data.ticks.length; i++) { if (TickUtil.isInitialized({ data: data.ticks[i] })) return data.ticks[i]; } } } static nextInitalizedTick({ data, currentTickIndex, tickSpacing, zeroForOne, }: { data: ReturnType<typeof TickArrayLayout.decode>; currentTickIndex: number; tickSpacing: number; zeroForOne: boolean; }) { const currentTickArrayStartIndex = this.getTickArrayStartIndex(currentTickIndex, tickSpacing); if (currentTickArrayStartIndex !== data.startTickIndex) return undefined; const offsetInArray = Math.floor((currentTickIndex - data.startTickIndex) / tickSpacing); if (zeroForOne) { for (let i = offsetInArray; i >= 0; i--) { if (TickUtil.isInitialized({ data: data.ticks[i] })) { return data.ticks[i]; } } } else { for (let i = offsetInArray + 1; i < TICK_ARRAY_SIZE; i++) { if (TickUtil.isInitialized({ data: data.ticks[i] })) { return data.ticks[i]; } } } return undefined; } static getTickArrayStartIndex(tickIndex: number, tickSpacing: number) { const ticksInArray = this.tickCount(tickSpacing); const start = Math.floor(tickIndex / ticksInArray); return start * ticksInArray; } static getTickOffsetInArray(tick: number, tickSpacing: number): number { if (tick % tickSpacing != 0) { throw new Error("tickIndex % tickSpacing not equal 0"); } const startIndex = this.getTickArrayStartIndex(tick, tickSpacing); return Math.floor((tick - startIndex) / tickSpacing); } static tickCount(tickSpacing: number) { return TICK_ARRAY_SIZE * tickSpacing; } static getMinTick(tickSpacing: number): number { return Math.ceil(MIN_TICK / tickSpacing) * tickSpacing; } static getMaxTick(tickSpacing: number): number { return Math.floor(MAX_TICK / tickSpacing) * tickSpacing; } } export class TickUtil { static isInitialized({ data }: { data: ReturnType<typeof TickLayout.decode> }): boolean { return this.hasLiquidity({ data }) || this.hasLimitOrders({ data }); } static hasLimitOrders({ data }: { data: ReturnType<typeof TickLayout.decode> }): boolean { return !data.ordersAmount.isZero() || !data.partFilledOrdersRemaining.isZero(); } static hasLiquidity({ data }: { data: ReturnType<typeof TickLayout.decode> }): boolean { return !data.liquidityGross.isZero(); } static isValidTick(tick: number): boolean { return tick >= MIN_TICK && tick <= MAX_TICK; } static checkTick(tick: number): void { if (!this.isValidTick(tick)) { throw new Error(`Tick ${tick} is out of range [${MIN_TICK}, ${MAX_TICK}]`); } } static getSqrtPriceAtTick(tick: number): BN { this.checkTick(tick); const absTick = Math.abs(tick); let ratio = Q64.clone(); for (const { bit, factor } of TICK_TO_SQRT_PRICE_FACTORS) { if ((absTick & (1 << bit)) !== 0) { ratio = mulDivFloor(ratio, factor, Q64); } } if (tick > 0) { ratio = mulDivFloor(Q64, Q64, ratio); } return ratio; } static getLimitOrderOutput({ amountIn, tick, zeroForOne }: { amountIn: BN; tick: number; zeroForOne: boolean }): BN { if (zeroForOne) { const priceX64 = TickUtil.getPriceAtTick(tick, false); return mulDivFloor(amountIn, priceX64, Q64); } else { const priceX64 = TickUtil.getPriceAtTick(tick, true); return mulDivFloor(amountIn, Q64, priceX64); } } static getLimitOrderInput({ amountOut, tick, zeroForOne }: { amountOut: BN; tick: number; zeroForOne: boolean }): BN { if (zeroForOne) { const priceX64 = TickUtil.getPriceAtTick(tick, true); return mulDivCeil(amountOut, priceX64, Q64); } else { const priceX64 = TickUtil.getPriceAtTick(tick, false); return mulDivCeil(amountOut, Q64, priceX64); } } static limitOrderUnfilledAmount({ tick }: { tick: ReturnType<typeof TickLayout.decode> }): BN { return tick.ordersAmount.add(tick.partFilledOrdersRemaining); } static matchLimitOrder({ tick, swapAmount, swapDirectionZeroForOne, isBaseInput, feeRate, isFeeOnInput, }: { tick: ReturnType<typeof TickLayout.decode>; swapAmount: BN; swapDirectionZeroForOne: boolean; isBaseInput: boolean; feeRate: number; isFeeOnInput: boolean; }): LimitOrderMatchResult { const result: LimitOrderMatchResult = { amountIn: BN_ZERO, amountOut: BN_ZERO, ammFeeAmount: BN_ZERO, }; const totalUnfilledAmount = this.limitOrderUnfilledAmount({ tick }); if (swapAmount.isZero() || totalUnfilledAmount.isZero()) { return result; } if (isBaseInput) { if (isFeeOnInput) { result.ammFeeAmount = mulDivCeil(swapAmount, new BN(feeRate), FEE_RATE_DENOMINATOR_VALUE); result.amountIn = swapAmount.sub(result.ammFeeAmount); } else { result.amountIn = swapAmount; } result.amountOut = this.getLimitOrderOutput({ amountIn: result.amountIn, tick: tick.tick, zeroForOne: swapDirectionZeroForOne, }); if (result.amountOut.gt(totalUnfilledAmount)) { result.amountOut = totalUnfilledAmount; result.amountIn = this.getLimitOrderInput({ amountOut: totalUnfilledAmount, tick: tick.tick, zeroForOne: !swapDirectionZeroForOne, }); if (isFeeOnInput) { result.ammFeeAmount = mulDivCeil( result.amountIn, new BN(feeRate), FEE_RATE_DENOMINATOR_VALUE.sub(new BN(feeRate)), ); } } } else { const netOutput = BN.min(swapAmount, totalUnfilledAmount); if (isFeeOnInput) { result.amountOut = netOutput; } else { result.amountOut = BN.min( mulDivCeil(netOutput, FEE_RATE_DENOMINATOR_VALUE, FEE_RATE_DENOMINATOR_VALUE.sub(new BN(feeRate))), totalUnfilledAmount, ); } result.amountIn = this.getLimitOrderInput({ amountOut: result.amountOut, tick: tick.tick, zeroForOne: !swapDirectionZeroForOne, }); if (isFeeOnInput) { result.ammFeeAmount = mulDivCeil( result.amountIn, new BN(feeRate), FEE_RATE_DENOMINATOR_VALUE.sub(new BN(feeRate)), ); } } let consumeFromPartRemaining = BN_ZERO; if (tick.partFilledOrdersRemaining.gt(BN_ZERO)) { consumeFromPartRemaining = BN.min(tick.partFilledOrdersRemaining, result.amountOut); if (consumeFromPartRemaining.gt(BN_ZERO)) { tick.unfilledRatioX64 = mulDivFloor(tick.unfilledRatioX64, tick.partFilledOrdersRemaining.sub(consumeFromPartRemaining), tick.partFilledOrdersRemaining) } tick.partFilledOrdersRemaining = tick.partFilledOrdersRemaining.sub(consumeFromPartRemaining); } const amountOutContinueToConsume = result.amountOut.sub(consumeFromPartRemaining); if (amountOutContinueToConsume.gt(BN_ZERO)) { if (!tick.partFilledOrdersRemaining.isZero()) throw Error("!tick.partFilledOrdersRemaining.isZero()"); if (tick.ordersAmount.lt(amountOutContinueToConsume)) throw Error("InvalidLimitOrderAmount"); tick.orderPhase = tick.orderPhase.add(BN_ONE); tick.unfilledRatioX64 = mulDivFloor(Q64, tick.ordersAmount.sub(amountOutContinueToConsume), tick.ordersAmount); tick.partFilledOrdersRemaining = tick.ordersAmount.sub(amountOutContinueToConsume); tick.ordersAmount = BN_ZERO; } if (!isFeeOnInput) { result.ammFeeAmount = mulDivCeil(result.amountOut, new BN(feeRate), FEE_RATE_DENOMINATOR_VALUE); result.amountOut = result.amountOut.sub(result.ammFeeAmount); } return result; } private static getPriceAtTick(tick: number, roundUp: boolean): BN { const sqrtPriceX64 = this.getSqrtPriceAtTick(tick); if (roundUp) { return sqrtPriceX64.mul(sqrtPriceX64).add(Q64.subn(1)).div(Q64); } else { return sqrtPriceX64.mul(sqrtPriceX64).div(Q64); } } static getTickAtSqrtPrice(sqrtPriceX64: BN): number { if (!(sqrtPriceX64.gte(MIN_SQRT_PRICE_X64) && sqrtPriceX64.lte(MAX_SQRT_PRICE_X64))) throw Error("SqrtPriceX64"); const msb = mostSignificantBit(sqrtPriceX64); const msbMinus64 = msb - 64; let log2pIntegerX32: BN; if (msbMinus64 >= 0) { log2pIntegerX32 = new BN(msbMinus64).shln(32); } else { log2pIntegerX32 = new BN(-msbMinus64).shln(32).neg(); } let r: BN; if (msb >= 64) { r = sqrtPriceX64.shrn(msb - 63); } else { r = sqrtPriceX64.shln(63 - msb); } let log2pFractionX64 = new BN(0); let bit = new BN(1).shln(63); for (let precision = 0; precision < BIT_PRECISION && !bit.isZero(); precision++) { r = r.mul(r); const isRMoreThanTwo = r.shrn(127).toNumber(); r = r.shrn(63 + isRMoreThanTwo); if (isRMoreThanTwo) { log2pFractionX64 = log2pFractionX64.add(bit); } bit = bit.shrn(1); } const log2pFractionX32 = log2pFractionX64.shrn(32); const log2pX32 = log2pIntegerX32.add(log2pFractionX32); const logSqrt10001X64 = log2pX32.mul(LOG_B_2_X32); const tickLowBN = logSqrt10001X64.sub(LOG_B_P_ERR_MARGIN_LOWER_X64); const tickHighBN = logSqrt10001X64.add(LOG_B_P_ERR_MARGIN_UPPER_X64); const tickLow = this.signedShrn64(tickLowBN); const tickHigh = this.signedShrn64(tickHighBN); if (tickLow === tickHigh) { return tickLow; } const sqrtPriceAtTickHigh = TickUtil.getSqrtPriceAtTick(tickHigh); if (sqrtPriceAtTickHigh.lte(sqrtPriceX64)) { return tickHigh; } return tickLow; } private static signedShrn64(bn: BN): number { if (bn.isNeg()) { const Q64 = new BN(1).shln(64); const result = bn.div(Q64); if (!bn.mod(Q64).isZero() && bn.isNeg()) { return result.subn(1).toNumber(); } return result.toNumber(); } else { return bn.shrn(64).toNumber(); } } static sqrtPriceX64ToPrice(sqrtPriceX64: BN, decimalsA: number, decimalsB: number): Decimal { const sqrtPriceSquared = sqrtPriceX64.mul(sqrtPriceX64); const decimalDiff = decimalsA - decimalsB; const DECIMAL_PRECISION = 20; const PRECISION_MULTIPLIER = new BN(10).pow(new BN(DECIMAL_PRECISION)); const numerator = sqrtPriceSquared.mul(PRECISION_MULTIPLIER); const denominator = new BN(1).shln(128); const scaledResult = numerator.div(denominator); let resultStr = scaledResult.toString(); while (resultStr.length <= DECIMAL_PRECISION) { resultStr = "0" + resultStr; } const integerPart = resultStr.slice(0, -DECIMAL_PRECISION); const decimalPart = resultStr.slice(-DECIMAL_PRECISION); const priceStr = integerPart + "." + decimalPart; const price = new Decimal(priceStr).mul(new Decimal(10).pow(decimalDiff)); return price; } static tickToPrice(tick: number, decimalsA: number, decimalsB: number): Decimal { const sqrtPriceX64 = TickUtil.getSqrtPriceAtTick(tick); return this.sqrtPriceX64ToPrice(sqrtPriceX64, decimalsA, decimalsB); } static priceToTick(price: Decimal, decimalsA: number, decimalsB: number): number { const adjustedPrice = price.div(Math.pow(10, decimalsA - decimalsB)); const tick = adjustedPrice.log().div(new Decimal(1.0001).log()).floor(); return Math.max(MIN_TICK, Math.min(MAX_TICK, tick.toNumber())); } static priceToSqrtPriceX64(price: Decimal, decimalsA: number, decimalsB: number): BN { const adjustedPrice = price.div(Math.pow(10, decimalsA - decimalsB)); const sqrtPrice = adjustedPrice.sqrt(); const sqrtPriceX64 = sqrtPrice.mul(new Decimal(2).pow(64)); return new BN(sqrtPriceX64.toFixed(0)); } static toTickIndex(tick: number, tickSpacing: number) { if (tick >= 0) { return tick - (tick % tickSpacing); } return tick - (tick % tickSpacing) - (tick % tickSpacing !== 0 ? tickSpacing : 0); } static getPriceAndTick({ price, mintADecimals, mintBDecimals, zeroForOne, tickSpacing, }: { price: Decimal; mintADecimals: number; mintBDecimals: number; zeroForOne: boolean; tickSpacing: number; }): { tick: number; price: Decimal } { let p = price.clamp(1 / 10 ** Math.max(mintADecimals, mintBDecimals), Number.MAX_SAFE_INTEGER); if (!zeroForOne) p = new Decimal(1).div(p); const newTick = TickUtil.toTickIndex(TickUtil.priceToTick(p, mintADecimals, mintBDecimals), tickSpacing); const newPrice = TickUtil.tickToPrice(newTick, mintADecimals, mintBDecimals); return { price: zeroForOne ? newPrice : new Decimal(1).div(newPrice), tick: newTick, }; } } /** * Fetch tick arrays for swap simulation */ export async function fetchTickArrays( programId: PublicKey, connection: Connection, poolId: PublicKey, currentTick: number, tickSpacing: number, tickArrayBitmap: Buffer, zeroForOne = true, ): Promise<{ address: PublicKey; value: ReturnType<typeof TickArrayLayout.decode> }[]> { const tickArrays: { address: PublicKey; value: ReturnType<typeof TickArrayLayout.decode> }[] = []; const tickArrayBitmapExtension = getPdaExBitmapAccount(programId, poolId).publicKey; const tickArrayBitmapExtensionRes = await connection.getAccountInfo(tickArrayBitmapExtension); const tickArraysAddress = TickArrayBitmapUtil.findTickArrayAddress({ programId, poolId, poolBitmap: tickArrayBitmap, tickArrayBitmap: TickArrayBitmapExtensionLayout.decode(tickArrayBitmapExtensionRes!.data), tickSpacing, findInfo: { type: zeroForOne ? "zeroForOne" : "oneForZero", tickArrayCurrent: currentTick }, }); const tickArrayRes = await getMultipleAccountsInfo(connection, tickArraysAddress); tickArrayRes.forEach((res, idx) => { if (res) tickArrays.push({ address: tickArraysAddress[idx], value: TickArrayLayout.decode(res.data) }); }); return tickArrays; }