@raydium-io/raydium-sdk-v2
Version:
An SDK for building applications on top of Raydium.
1,381 lines (1,242 loc) • 45.4 kB
text/typescript
import { Connection, EpochInfo, PublicKey } from "@solana/web3.js";
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import BN from "bn.js";
import {
ClmmPoolInfo,
ClmmPoolRewardInfo,
ComputeClmmPoolInfo,
ReturnTypeComputeAmountOut,
ReturnTypeComputeAmountOutBaseOut,
ReturnTypeComputeAmountOutFormat,
ReturnTypeFetchExBitmaps,
ReturnTypeFetchMultiplePoolTickArrays,
ReturnTypeGetLiquidityAmountOut,
SDKParsedConcentratedInfo,
} from "../type";
import { ApiV3PoolInfoConcentratedItem, ApiV3Token } from "@/api/type";
import Decimal from "decimal.js";
import {
getMultipleAccountsInfo,
getMultipleAccountsInfoWithCustomFlags,
getTransferAmountFeeV2,
minExpirationTime,
solToWSol,
} from "@/common";
import { Percent, Price, Token, TokenAmount } from "@/module";
import { TokenAccountRaw } from "@/raydium/account/types";
import {
PoolInfoLayout,
PositionInfoLayout,
RewardInfo,
TickArrayBitmapExtensionLayout,
TickArrayLayout,
} from "../layout";
import { MAX_SQRT_PRICE_X64, MAX_TICK, MIN_SQRT_PRICE_X64, MIN_TICK, NEGATIVE_ONE, Q64, ZERO } from "./constants";
import { LiquidityMath, MathUtil, SqrtPriceMath, SwapMath } from "./math";
import { getPdaExBitmapAccount, getPdaPersonalPositionAddress, getPdaTickArrayAddress } from "./pda";
import { PositionUtils } from "./position";
import { TICK_ARRAY_BITMAP_SIZE, Tick, TickArray, TickUtils } from "./tick";
import { TickArrayBitmap, TickArrayBitmapExtensionUtils } from "./tickarrayBitmap";
import { TickQuery } from "./tickQuery";
export class PoolUtils {
public static getOutputAmountAndRemainAccounts(
poolInfo: ComputeClmmPoolInfo,
tickArrayCache: { [key: string]: TickArray },
inputTokenMint: PublicKey,
inputAmount: BN,
sqrtPriceLimitX64?: BN,
catchLiquidityInsufficient = false,
): {
allTrade: boolean;
expectedAmountOut: BN;
remainingAccounts: PublicKey[];
executionPrice: BN;
feeAmount: BN;
} {
const zeroForOne = inputTokenMint.toBase58() === poolInfo.mintA.address;
const allNeededAccounts: PublicKey[] = [];
const {
isExist,
startIndex: firstTickArrayStartIndex,
nextAccountMeta,
} = this.getFirstInitializedTickArray(poolInfo, zeroForOne);
if (!isExist || firstTickArrayStartIndex === undefined || !nextAccountMeta) throw new Error("Invalid tick array");
// try {
// const preTick = this.preInitializedTickArrayStartIndex(poolInfo, !zeroForOne)
// if (preTick.isExist) {
// const { publicKey: address } = getPdaTickArrayAddress(
// poolInfo.programId,
// poolInfo.id,
// preTick.nextStartIndex
// );
// allNeededAccounts.push(address)
// }
// } catch (e) { /* empty */ }
allNeededAccounts.push(nextAccountMeta);
const {
allTrade,
amountCalculated: outputAmount,
accounts: reaminAccounts,
sqrtPriceX64: executionPrice,
feeAmount,
} = SwapMath.swapCompute(
poolInfo.programId,
poolInfo.id,
tickArrayCache,
poolInfo.tickArrayBitmap,
poolInfo.exBitmapInfo,
zeroForOne,
poolInfo.ammConfig.tradeFeeRate,
poolInfo.liquidity,
poolInfo.tickCurrent,
poolInfo.tickSpacing,
poolInfo.sqrtPriceX64,
inputAmount,
firstTickArrayStartIndex,
sqrtPriceLimitX64,
catchLiquidityInsufficient,
);
allNeededAccounts.push(...reaminAccounts);
return {
allTrade,
expectedAmountOut: outputAmount.mul(NEGATIVE_ONE),
remainingAccounts: allNeededAccounts,
executionPrice,
feeAmount,
};
}
public static getInputAmountAndRemainAccounts(
poolInfo: ComputeClmmPoolInfo,
tickArrayCache: { [key: string]: TickArray },
outputTokenMint: PublicKey,
outputAmount: BN,
sqrtPriceLimitX64?: BN,
): { expectedAmountIn: BN; remainingAccounts: PublicKey[]; executionPrice: BN; feeAmount: BN } {
const zeroForOne = outputTokenMint.toBase58() === poolInfo.mintB.address;
const allNeededAccounts: PublicKey[] = [];
const {
isExist,
startIndex: firstTickArrayStartIndex,
nextAccountMeta,
} = this.getFirstInitializedTickArray(poolInfo, zeroForOne);
if (!isExist || firstTickArrayStartIndex === undefined || !nextAccountMeta) throw new Error("Invalid tick array");
try {
const preTick = this.preInitializedTickArrayStartIndex(poolInfo, zeroForOne);
if (preTick.isExist) {
const { publicKey: address } = getPdaTickArrayAddress(poolInfo.programId, poolInfo.id, preTick.nextStartIndex);
allNeededAccounts.push(address);
}
} catch (e) {
/* empty */
}
allNeededAccounts.push(nextAccountMeta);
const {
amountCalculated: inputAmount,
accounts: reaminAccounts,
sqrtPriceX64: executionPrice,
feeAmount,
} = SwapMath.swapCompute(
poolInfo.programId,
poolInfo.id,
tickArrayCache,
poolInfo.tickArrayBitmap,
poolInfo.exBitmapInfo,
zeroForOne,
poolInfo.ammConfig.tradeFeeRate,
poolInfo.liquidity,
poolInfo.tickCurrent,
poolInfo.tickSpacing,
poolInfo.sqrtPriceX64,
outputAmount.mul(NEGATIVE_ONE),
firstTickArrayStartIndex,
sqrtPriceLimitX64,
);
allNeededAccounts.push(...reaminAccounts);
return { expectedAmountIn: inputAmount, remainingAccounts: allNeededAccounts, executionPrice, feeAmount };
}
public static getFirstInitializedTickArray(
poolInfo: ComputeClmmPoolInfo,
zeroForOne: boolean,
):
| { isExist: true; startIndex: number; nextAccountMeta: PublicKey }
| { isExist: false; startIndex: undefined; nextAccountMeta: undefined } {
const { isInitialized, startIndex } = PoolUtils.isOverflowDefaultTickarrayBitmap(poolInfo.tickSpacing, [
poolInfo.tickCurrent,
])
? TickArrayBitmapExtensionUtils.checkTickArrayIsInit(
TickQuery.getArrayStartIndex(poolInfo.tickCurrent, poolInfo.tickSpacing),
poolInfo.tickSpacing,
poolInfo.exBitmapInfo,
)
: TickUtils.checkTickArrayIsInitialized(
TickUtils.mergeTickArrayBitmap(poolInfo.tickArrayBitmap),
poolInfo.tickCurrent,
poolInfo.tickSpacing,
);
if (isInitialized) {
const { publicKey: address } = getPdaTickArrayAddress(poolInfo.programId, poolInfo.id, startIndex);
return {
isExist: true,
startIndex,
nextAccountMeta: address,
};
}
const { isExist, nextStartIndex } = this.nextInitializedTickArrayStartIndex(
poolInfo,
TickQuery.getArrayStartIndex(poolInfo.tickCurrent, poolInfo.tickSpacing),
zeroForOne,
);
if (isExist) {
const { publicKey: address } = getPdaTickArrayAddress(poolInfo.programId, poolInfo.id, nextStartIndex);
return {
isExist: true,
startIndex: nextStartIndex,
nextAccountMeta: address,
};
}
return { isExist: false, nextAccountMeta: undefined, startIndex: undefined };
}
public static preInitializedTickArrayStartIndex(
poolInfo: ComputeClmmPoolInfo,
zeroForOne: boolean,
): { isExist: boolean; nextStartIndex: number } {
const currentOffset = Math.floor(poolInfo.tickCurrent / TickQuery.tickCount(poolInfo.tickSpacing));
const result: number[] = !zeroForOne
? TickUtils.searchLowBitFromStart(
poolInfo.tickArrayBitmap,
poolInfo.exBitmapInfo,
currentOffset - 1,
1,
poolInfo.tickSpacing,
)
: TickUtils.searchHightBitFromStart(
poolInfo.tickArrayBitmap,
poolInfo.exBitmapInfo,
currentOffset + 1,
1,
poolInfo.tickSpacing,
);
return result.length > 0 ? { isExist: true, nextStartIndex: result[0] } : { isExist: false, nextStartIndex: 0 };
}
public static nextInitializedTickArrayStartIndex(
poolInfo:
| {
tickCurrent: number;
tickSpacing: number;
tickArrayBitmap: BN[];
exBitmapInfo: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>;
}
| ClmmPoolInfo,
lastTickArrayStartIndex: number,
zeroForOne: boolean,
): { isExist: boolean; nextStartIndex: number } {
lastTickArrayStartIndex = TickQuery.getArrayStartIndex(poolInfo.tickCurrent, poolInfo.tickSpacing);
// eslint-disable-next-line no-constant-condition
while (true) {
const { isInit: startIsInit, tickIndex: startIndex } = TickArrayBitmap.nextInitializedTickArrayStartIndex(
TickUtils.mergeTickArrayBitmap(poolInfo.tickArrayBitmap),
lastTickArrayStartIndex,
poolInfo.tickSpacing,
zeroForOne,
);
if (startIsInit) {
return { isExist: true, nextStartIndex: startIndex };
}
lastTickArrayStartIndex = startIndex;
const { isInit, tickIndex } = TickArrayBitmapExtensionUtils.nextInitializedTickArrayFromOneBitmap(
lastTickArrayStartIndex,
poolInfo.tickSpacing,
zeroForOne,
poolInfo.exBitmapInfo,
);
if (isInit) return { isExist: true, nextStartIndex: tickIndex };
lastTickArrayStartIndex = tickIndex;
if (lastTickArrayStartIndex < MIN_TICK || lastTickArrayStartIndex > MAX_TICK)
return { isExist: false, nextStartIndex: 0 };
}
// const tickArrayBitmap = TickUtils.mergeTickArrayBitmap(
// poolInfo.tickArrayBitmap
// );
// const currentOffset = TickUtils.getTickArrayOffsetInBitmapByTick(
// poolInfo.tickCurrent,
// poolInfo.tickSpacing
// );
// const result: number[] = zeroForOne ? TickUtils.searchLowBitFromStart(
// tickArrayBitmap,
// currentOffset - 1,
// 0,
// 1,
// poolInfo.tickSpacing
// ) : TickUtils.searchHightBitFromStart(
// tickArrayBitmap,
// currentOffset,
// 1024,
// 1,
// poolInfo.tickSpacing
// );
// return result.length > 0 ? { isExist: true, nextStartIndex: result[0] } : { isExist: false, nextStartIndex: 0 }
}
public static async updatePoolRewardInfos({
connection,
apiPoolInfo,
chainTime,
poolLiquidity,
rewardInfos,
}: {
connection: Connection;
apiPoolInfo: ApiV3PoolInfoConcentratedItem;
chainTime: number;
poolLiquidity: BN;
rewardInfos: ReturnType<typeof RewardInfo.decode>[];
}): Promise<ClmmPoolRewardInfo[]> {
const nRewardInfo: ClmmPoolRewardInfo[] = [];
for (let i = 0; i < rewardInfos.length; i++) {
const _itemReward = rewardInfos[i];
const apiRewardProgram =
apiPoolInfo.rewardDefaultInfos[i]?.mint.programId ??
(await connection.getAccountInfo(_itemReward.tokenMint))?.owner;
if (apiRewardProgram === undefined) throw Error("get new reward mint info error");
const itemReward: ClmmPoolRewardInfo = {
..._itemReward,
perSecond: MathUtil.x64ToDecimal(_itemReward.emissionsPerSecondX64),
remainingRewards: undefined,
tokenProgramId: new PublicKey(apiRewardProgram),
};
if (itemReward.tokenMint.equals(PublicKey.default)) continue;
if (chainTime <= itemReward.openTime.toNumber() || poolLiquidity.eq(ZERO)) {
nRewardInfo.push(itemReward);
continue;
}
const latestUpdateTime = new BN(Math.min(itemReward.endTime.toNumber(), chainTime));
const timeDelta = latestUpdateTime.sub(itemReward.lastUpdateTime);
const rewardGrowthDeltaX64 = MathUtil.mulDivFloor(timeDelta, itemReward.emissionsPerSecondX64, poolLiquidity);
const rewardGrowthGlobalX64 = itemReward.rewardGrowthGlobalX64.add(rewardGrowthDeltaX64);
const rewardEmissionedDelta = MathUtil.mulDivFloor(timeDelta, itemReward.emissionsPerSecondX64, Q64);
const rewardTotalEmissioned = itemReward.rewardTotalEmissioned.add(rewardEmissionedDelta);
nRewardInfo.push({
...itemReward,
rewardGrowthGlobalX64,
rewardTotalEmissioned,
lastUpdateTime: latestUpdateTime,
});
}
return nRewardInfo;
}
public static isOverflowDefaultTickarrayBitmap(tickSpacing: number, tickarrayStartIndexs: number[]): boolean {
const { maxTickBoundary, minTickBoundary } = this.tickRange(tickSpacing);
for (const tickIndex of tickarrayStartIndexs) {
const tickarrayStartIndex = TickUtils.getTickArrayStartIndexByTick(tickIndex, tickSpacing);
if (tickarrayStartIndex >= maxTickBoundary || tickarrayStartIndex < minTickBoundary) {
return true;
}
}
return false;
}
public static tickRange(tickSpacing: number): {
maxTickBoundary: number;
minTickBoundary: number;
} {
let maxTickBoundary = TickArrayBitmap.maxTickInTickarrayBitmap(tickSpacing);
let minTickBoundary = -maxTickBoundary;
if (maxTickBoundary > MAX_TICK) {
maxTickBoundary = TickQuery.getArrayStartIndex(MAX_TICK, tickSpacing) + TickQuery.tickCount(tickSpacing);
}
if (minTickBoundary < MIN_TICK) {
minTickBoundary = TickQuery.getArrayStartIndex(MIN_TICK, tickSpacing);
}
return { maxTickBoundary, minTickBoundary };
}
public static get_tick_array_offset(tickarrayStartIndex: number, tickSpacing: number): number {
if (!TickQuery.checkIsValidStartIndex(tickarrayStartIndex, tickSpacing)) {
throw new Error("No enough initialized tickArray");
}
return (tickarrayStartIndex / TickQuery.tickCount(tickSpacing)) * TICK_ARRAY_BITMAP_SIZE;
}
static async fetchExBitmaps({
connection,
exBitmapAddress,
batchRequest,
}: {
connection: Connection;
exBitmapAddress: PublicKey[];
batchRequest: boolean;
}): Promise<ReturnTypeFetchExBitmaps> {
const fetchedBitmapAccount = await getMultipleAccountsInfoWithCustomFlags(
connection,
exBitmapAddress.map((i) => ({ pubkey: i })),
{ batchRequest },
);
const returnTypeFetchExBitmaps: ReturnTypeFetchExBitmaps = {};
for (const item of fetchedBitmapAccount) {
if (item.accountInfo === null) continue;
returnTypeFetchExBitmaps[item.pubkey.toString()] = TickArrayBitmapExtensionLayout.decode(item.accountInfo.data);
}
return returnTypeFetchExBitmaps;
}
static async fetchMultiplePoolTickArrays({
connection,
poolKeys,
batchRequest,
}: {
connection: Connection;
poolKeys: Omit<ComputeClmmPoolInfo, "ammConfig">[];
batchRequest?: boolean;
}): Promise<ReturnTypeFetchMultiplePoolTickArrays> {
const tickArraysToPoolId: { [key: string]: PublicKey } = {};
const tickArrays: { pubkey: PublicKey }[] = [];
for (const itemPoolInfo of poolKeys) {
const currentTickArrayStartIndex = TickUtils.getTickArrayStartIndexByTick(
itemPoolInfo.tickCurrent,
itemPoolInfo.tickSpacing,
);
const startIndexArray = TickUtils.getInitializedTickArrayInRange(
itemPoolInfo.tickArrayBitmap,
itemPoolInfo.exBitmapInfo,
itemPoolInfo.tickSpacing,
currentTickArrayStartIndex,
7,
);
for (const itemIndex of startIndexArray) {
const { publicKey: tickArrayAddress } = getPdaTickArrayAddress(
itemPoolInfo.programId,
itemPoolInfo.id,
itemIndex,
);
tickArrays.push({ pubkey: tickArrayAddress });
tickArraysToPoolId[tickArrayAddress.toString()] = itemPoolInfo.id;
}
}
const fetchedTickArrays = await getMultipleAccountsInfoWithCustomFlags(connection, tickArrays, { batchRequest });
const tickArrayCache: ReturnTypeFetchMultiplePoolTickArrays = {};
for (const itemAccountInfo of fetchedTickArrays) {
if (!itemAccountInfo.accountInfo) continue;
const poolId = tickArraysToPoolId[itemAccountInfo.pubkey.toString()];
if (!poolId) continue;
if (tickArrayCache[poolId.toString()] === undefined) tickArrayCache[poolId.toString()] = {};
const accountLayoutData = TickArrayLayout.decode(itemAccountInfo.accountInfo.data);
tickArrayCache[poolId.toString()][accountLayoutData.startTickIndex] = {
...accountLayoutData,
address: itemAccountInfo.pubkey,
};
}
return tickArrayCache;
}
// deprecated, new api doesn't need
static async fetchPoolsAccountPosition({
pools,
connection,
ownerInfo,
batchRequest = false,
updateOwnerRewardAndFee = true,
}: {
pools: SDKParsedConcentratedInfo[];
connection: Connection;
ownerInfo: { wallet: PublicKey; tokenAccounts: TokenAccountRaw[] };
batchRequest?: boolean;
updateOwnerRewardAndFee?: boolean;
}): Promise<SDKParsedConcentratedInfo[]> {
const programIds: PublicKey[] = [];
for (let index = 0; index < pools.length; index++) {
const accountInfo = pools[index];
if (accountInfo === null) continue;
if (!programIds.find((i) => i.equals(accountInfo.state.programId))) programIds.push(accountInfo.state.programId);
}
if (ownerInfo) {
const allMint = ownerInfo.tokenAccounts.map((i) => i.accountInfo.mint);
const allPositionKey: PublicKey[] = [];
for (const itemMint of allMint) {
for (const itemProgramId of programIds) {
allPositionKey.push(getPdaPersonalPositionAddress(itemProgramId, itemMint).publicKey);
}
}
const positionAccountInfos = await getMultipleAccountsInfo(connection, allPositionKey, { batchRequest });
const keyToTickArrayAddress: { [key: string]: PublicKey } = {};
for (const itemAccountInfo of positionAccountInfos) {
if (itemAccountInfo === null) continue;
// TODO: add check
const position = PositionInfoLayout.decode(itemAccountInfo.data);
const itemPoolId = position.poolId.toString();
const poolInfoA = pools.find((pool) => pool.state.id.toBase58() === itemPoolId);
if (poolInfoA === undefined) continue;
const poolInfo = poolInfoA.state;
const priceLower = TickUtils._getTickPriceLegacy({
poolInfo,
tick: position.tickLower,
baseIn: true,
});
const priceUpper = TickUtils._getTickPriceLegacy({
poolInfo,
tick: position.tickUpper,
baseIn: true,
});
const { amountA, amountB } = LiquidityMath.getAmountsFromLiquidity(
poolInfo.sqrtPriceX64,
priceLower.tickSqrtPriceX64,
priceUpper.tickSqrtPriceX64,
position.liquidity,
false,
);
const leverage = 1 / (1 - Math.sqrt(Math.sqrt(priceLower.price.div(priceUpper.price).toNumber())));
poolInfoA.positionAccount = [
...(poolInfoA.positionAccount ?? []),
{
poolId: position.poolId,
nftMint: position.nftMint,
priceLower: priceLower.price,
priceUpper: priceUpper.price,
amountA,
amountB,
tickLower: position.tickLower,
tickUpper: position.tickUpper,
liquidity: position.liquidity,
feeGrowthInsideLastX64A: position.feeGrowthInsideLastX64A,
feeGrowthInsideLastX64B: position.feeGrowthInsideLastX64B,
tokenFeesOwedA: position.tokenFeesOwedA,
tokenFeesOwedB: position.tokenFeesOwedB,
rewardInfos: position.rewardInfos.map((i) => ({
...i,
pendingReward: new BN(0),
})),
leverage,
tokenFeeAmountA: new BN(0),
tokenFeeAmountB: new BN(0),
},
];
const tickArrayLowerAddress = await TickUtils.getTickArrayAddressByTick(
poolInfoA.state.programId,
position.poolId,
position.tickLower,
poolInfoA.state.tickSpacing,
);
const tickArrayUpperAddress = await TickUtils.getTickArrayAddressByTick(
poolInfoA.state.programId,
position.poolId,
position.tickUpper,
poolInfoA.state.tickSpacing,
);
keyToTickArrayAddress[
`${poolInfoA.state.programId.toString()}-${position.poolId.toString()}-${position.tickLower}`
] = tickArrayLowerAddress;
keyToTickArrayAddress[
`${poolInfoA.state.programId.toString()}-${position.poolId.toString()}-${position.tickUpper}`
] = tickArrayUpperAddress;
}
if (updateOwnerRewardAndFee) {
const tickArrayKeys = Object.values(keyToTickArrayAddress);
const tickArrayDatas = await getMultipleAccountsInfo(connection, tickArrayKeys, { batchRequest });
const tickArrayLayout = {};
for (let index = 0; index < tickArrayKeys.length; index++) {
const tickArrayData = tickArrayDatas[index];
if (tickArrayData === null) continue;
const key = tickArrayKeys[index].toString();
tickArrayLayout[key] = TickArrayLayout.decode(tickArrayData.data);
}
for (const { state, positionAccount } of pools) {
if (!positionAccount) continue;
for (const itemPA of positionAccount) {
const keyLower = `${state.programId.toString()}-${state.id.toString()}-${itemPA.tickLower}`;
const keyUpper = `${state.programId.toString()}-${state.id.toString()}-${itemPA.tickUpper}`;
const tickArrayLower = tickArrayLayout[keyToTickArrayAddress[keyLower].toString()];
const tickArrayUpper = tickArrayLayout[keyToTickArrayAddress[keyUpper].toString()];
const tickLowerState: Tick =
tickArrayLower.ticks[TickUtils.getTickOffsetInArray(itemPA.tickLower, state.tickSpacing)];
const tickUpperState: Tick =
tickArrayUpper.ticks[TickUtils.getTickOffsetInArray(itemPA.tickUpper, state.tickSpacing)];
const { tokenFeeAmountA, tokenFeeAmountB } = await PositionUtils.GetPositionFees(
state,
itemPA,
tickLowerState,
tickUpperState,
);
const rewardInfos = await PositionUtils.GetPositionRewards(state, itemPA, tickLowerState, tickUpperState);
itemPA.tokenFeeAmountA = tokenFeeAmountA.gte(new BN(0)) ? tokenFeeAmountA : new BN(0);
itemPA.tokenFeeAmountB = tokenFeeAmountB.gte(new BN(0)) ? tokenFeeAmountB : new BN(0);
for (let i = 0; i < rewardInfos.length; i++) {
itemPA.rewardInfos[i].pendingReward = rewardInfos[i].gte(new BN(0)) ? rewardInfos[i] : new BN(0);
}
}
}
}
}
return pools;
}
static computeAmountOut({
poolInfo,
tickArrayCache,
baseMint,
epochInfo,
amountIn,
slippage,
priceLimit = new Decimal(0),
catchLiquidityInsufficient = false,
}: {
poolInfo: ComputeClmmPoolInfo;
tickArrayCache: { [key: string]: TickArray };
baseMint: PublicKey;
epochInfo: EpochInfo;
amountIn: BN;
slippage: number;
priceLimit?: Decimal;
catchLiquidityInsufficient: boolean;
}): ReturnTypeComputeAmountOut {
let sqrtPriceLimitX64: BN;
const isBaseIn = baseMint.toBase58() === poolInfo.mintA.address;
const [baseFeeConfig, outFeeConfig] = isBaseIn
? [poolInfo.mintA.extensions.feeConfig, poolInfo.mintB.extensions.feeConfig]
: [poolInfo.mintB.extensions.feeConfig, poolInfo.mintA.extensions.feeConfig];
if (priceLimit.equals(new Decimal(0))) {
sqrtPriceLimitX64 = isBaseIn ? MIN_SQRT_PRICE_X64.add(new BN(1)) : MAX_SQRT_PRICE_X64.sub(new BN(1));
} else {
sqrtPriceLimitX64 = SqrtPriceMath.priceToSqrtPriceX64(
priceLimit,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
}
const realAmountIn = getTransferAmountFeeV2(amountIn, baseFeeConfig, epochInfo, false);
const {
allTrade,
expectedAmountOut: _expectedAmountOut,
remainingAccounts,
executionPrice: _executionPriceX64,
feeAmount,
} = PoolUtils.getOutputAmountAndRemainAccounts(
poolInfo,
tickArrayCache,
baseMint,
realAmountIn.amount.sub(realAmountIn.fee ?? ZERO),
sqrtPriceLimitX64,
catchLiquidityInsufficient,
);
const amountOut = getTransferAmountFeeV2(_expectedAmountOut, outFeeConfig, epochInfo, false);
const _executionPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
_executionPriceX64,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const executionPrice = isBaseIn ? _executionPrice : new Decimal(1).div(_executionPrice);
const _minAmountOut = _expectedAmountOut
.mul(new BN(Math.floor((1 - slippage) * 10000000000)))
.div(new BN(10000000000));
const minAmountOut = getTransferAmountFeeV2(_minAmountOut, outFeeConfig, epochInfo, false);
const poolPrice = isBaseIn ? poolInfo.currentPrice : new Decimal(1).div(poolInfo.currentPrice);
const _numerator = new Decimal(executionPrice).sub(poolPrice).abs();
const _denominator = poolPrice;
const priceImpact = new Percent(
new Decimal(_numerator).mul(10 ** 15).toFixed(0),
new Decimal(_denominator).mul(10 ** 15).toFixed(0),
);
return {
allTrade,
realAmountIn,
amountOut,
minAmountOut,
expirationTime: minExpirationTime(realAmountIn.expirationTime, amountOut.expirationTime),
currentPrice: poolInfo.currentPrice,
executionPrice,
priceImpact,
fee: feeAmount,
remainingAccounts,
executionPriceX64: _executionPriceX64,
};
}
static computeAmountOutFormat({
poolInfo,
tickArrayCache,
amountIn,
tokenOut: _tokenOut,
slippage,
epochInfo,
catchLiquidityInsufficient = false,
}: {
poolInfo: ComputeClmmPoolInfo;
tickArrayCache: { [key: string]: TickArray };
amountIn: BN;
tokenOut: ApiV3Token;
slippage: number;
epochInfo: EpochInfo;
catchLiquidityInsufficient?: boolean;
}): ReturnTypeComputeAmountOutFormat {
const baseIn = _tokenOut.address === poolInfo.mintB.address;
const [inputMint, outMint] = baseIn ? [poolInfo.mintA, poolInfo.mintB] : [poolInfo.mintB, poolInfo.mintA];
const [baseToken, outToken] = [
new Token({
...inputMint,
mint: inputMint.address,
isToken2022: inputMint.programId === TOKEN_2022_PROGRAM_ID.toBase58(),
}),
new Token({
...outMint,
mint: outMint.address,
isToken2022: outMint.programId === TOKEN_2022_PROGRAM_ID.toBase58(),
}),
];
const {
allTrade,
realAmountIn: _realAmountIn,
amountOut: _amountOut,
minAmountOut: _minAmountOut,
expirationTime,
currentPrice,
executionPrice,
priceImpact,
fee,
remainingAccounts,
executionPriceX64,
} = PoolUtils.computeAmountOut({
poolInfo,
tickArrayCache,
baseMint: new PublicKey(inputMint.address),
amountIn,
slippage,
epochInfo,
catchLiquidityInsufficient,
});
const realAmountIn = {
..._realAmountIn,
amount: new TokenAmount(baseToken, _realAmountIn.amount),
fee: _realAmountIn.fee === undefined ? undefined : new TokenAmount(baseToken, _realAmountIn.fee),
};
const amountOut = {
..._amountOut,
amount: new TokenAmount(outToken, _amountOut.amount),
fee: _amountOut.fee === undefined ? undefined : new TokenAmount(outToken, _amountOut.fee),
};
const minAmountOut = {
..._minAmountOut,
amount: new TokenAmount(outToken, _minAmountOut.amount),
fee: _minAmountOut.fee === undefined ? undefined : new TokenAmount(outToken, _minAmountOut.fee),
};
const _currentPrice = new Price({
baseToken,
denominator: new BN(10).pow(new BN(20 + baseToken.decimals)),
quoteToken: outToken,
numerator: currentPrice.mul(new Decimal(10 ** (20 + outToken.decimals))).toFixed(0),
});
const _executionPrice = new Price({
baseToken,
denominator: new BN(10).pow(new BN(20 + baseToken.decimals)),
quoteToken: outToken,
numerator: executionPrice.mul(new Decimal(10 ** (20 + outToken.decimals))).toFixed(0),
});
const _fee = new TokenAmount(baseToken, fee);
return {
allTrade,
realAmountIn,
amountOut,
minAmountOut,
expirationTime,
currentPrice: _currentPrice,
executionPrice: _executionPrice,
priceImpact,
fee: _fee,
remainingAccounts,
executionPriceX64,
};
}
static computeAmountIn({
poolInfo,
tickArrayCache,
baseMint,
epochInfo,
amountOut,
slippage,
priceLimit = new Decimal(0),
}: {
poolInfo: ComputeClmmPoolInfo;
tickArrayCache: { [key: string]: TickArray };
baseMint: PublicKey;
epochInfo: EpochInfo;
amountOut: BN;
slippage: number;
priceLimit?: Decimal;
}): ReturnTypeComputeAmountOutBaseOut {
const isBaseIn = baseMint.toBase58() === poolInfo.mintA.address;
const feeConfigs = {
[poolInfo.mintA.address]: poolInfo.mintA.extensions.feeConfig,
[poolInfo.mintB.address]: poolInfo.mintB.extensions.feeConfig,
};
let sqrtPriceLimitX64: BN;
if (priceLimit.equals(new Decimal(0))) {
sqrtPriceLimitX64 = !isBaseIn ? MIN_SQRT_PRICE_X64.add(new BN(1)) : MAX_SQRT_PRICE_X64.sub(new BN(1));
} else {
sqrtPriceLimitX64 = SqrtPriceMath.priceToSqrtPriceX64(
priceLimit,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
}
const realAmountOut = getTransferAmountFeeV2(amountOut, feeConfigs[baseMint.toString()], epochInfo, true);
const {
expectedAmountIn: _expectedAmountIn,
remainingAccounts,
executionPrice: _executionPriceX64,
feeAmount,
} = PoolUtils.getInputAmountAndRemainAccounts(
poolInfo,
tickArrayCache,
baseMint,
realAmountOut.amount.sub(realAmountOut.fee ?? ZERO),
sqrtPriceLimitX64,
);
const inMint = isBaseIn ? poolInfo.mintB.address : poolInfo.mintA.address;
const amountIn = getTransferAmountFeeV2(_expectedAmountIn, feeConfigs[inMint], epochInfo, false);
// const amountIn = getTransferAmountFee(
// _expectedAmountIn,
// token2022Infos[inMint.toString()]?.feeConfig,
// epochInfo,
// true,
// );
const _executionPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
_executionPriceX64,
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const executionPrice = isBaseIn ? _executionPrice : new Decimal(1).div(_executionPrice);
const _maxAmountIn = _expectedAmountIn
.mul(new BN(Math.floor((1 + slippage) * 10000000000)))
.div(new BN(10000000000));
// const maxAmountIn = getTransferAmountFee(
// _maxAmountIn,
// token2022Infos[inMint.toString()]?.feeConfig,
// epochInfo,
// true,
// );
const maxAmountIn = getTransferAmountFeeV2(_maxAmountIn, feeConfigs[inMint], epochInfo, true);
const poolPrice = isBaseIn ? poolInfo.currentPrice : new Decimal(1).div(poolInfo.currentPrice);
const _numerator = new Decimal(executionPrice).sub(poolPrice).abs();
const _denominator = poolPrice;
const priceImpact = new Percent(
new Decimal(_numerator).mul(10 ** 15).toFixed(0),
new Decimal(_denominator).mul(10 ** 15).toFixed(0),
);
return {
amountIn,
maxAmountIn,
realAmountOut,
expirationTime: minExpirationTime(amountIn.expirationTime, realAmountOut.expirationTime),
currentPrice: poolInfo.currentPrice,
executionPrice,
priceImpact,
fee: feeAmount,
remainingAccounts,
};
}
static estimateAprsForPriceRangeMultiplier({
poolInfo,
aprType,
positionTickLowerIndex,
positionTickUpperIndex,
}: {
poolInfo: ApiV3PoolInfoConcentratedItem;
aprType: "day" | "week" | "month";
positionTickLowerIndex: number;
positionTickUpperIndex: number;
}): {
feeApr: number;
rewardsApr: number[];
apr: number;
} {
const aprInfo = poolInfo[aprType];
const priceLower = TickUtils.getTickPrice({
poolInfo,
tick: positionTickLowerIndex,
baseIn: true,
}).price.toNumber();
const priceUpper = TickUtils.getTickPrice({
poolInfo,
tick: positionTickUpperIndex,
baseIn: true,
}).price.toNumber();
const _minPrice = Math.max(priceLower, aprInfo.priceMin);
const _maxPrice = Math.min(priceUpper, aprInfo.priceMax);
const sub = _maxPrice - _minPrice;
const userRange = priceUpper - priceLower;
const tradeRange = aprInfo.priceMax - aprInfo.priceMin;
let p: number;
if (sub <= 0) p = 0;
else if (userRange === sub) p = tradeRange / sub;
else if (tradeRange === sub) p = sub / userRange;
else p = (sub / tradeRange) * (sub / userRange);
return {
feeApr: aprInfo.feeApr * p,
rewardsApr: [(aprInfo.rewardApr[0] ?? 0) * p, (aprInfo.rewardApr[1] ?? 0) * p, (aprInfo.rewardApr[2] ?? 0) * p],
apr: aprInfo.apr * p,
};
}
static estimateAprsForPriceRangeDelta({
poolInfo,
poolLiquidity,
aprType,
mintPrice,
liquidity,
positionTickLowerIndex,
positionTickUpperIndex,
chainTime,
}: {
poolInfo: ApiV3PoolInfoConcentratedItem;
poolLiquidity: BN;
aprType: "day" | "week" | "month";
mintPrice: { [mint: string]: { value: number } };
liquidity: BN;
positionTickLowerIndex: number;
positionTickUpperIndex: number;
chainTime: number;
}): {
feeApr: number;
rewardsApr: number[];
apr: number;
} {
const aprTypeDay = aprType === "day" ? 1 : aprType === "week" ? 7 : aprType === "month" ? 30 : 0;
const aprInfo = poolInfo[aprType];
const mintPriceA = mintPrice[solToWSol(poolInfo.mintA.address).toString()];
const mintPriceB = mintPrice[solToWSol(poolInfo.mintB.address).toString()];
const mintDecimalsA = poolInfo.mintA.decimals;
const mintDecimalsB = poolInfo.mintB.decimals;
if (!aprInfo || !mintPriceA || !mintPriceB) return { feeApr: 0, rewardsApr: [0, 0, 0], apr: 0 };
const sqrtPriceX64 = SqrtPriceMath.priceToSqrtPriceX64(
new Decimal(poolInfo.price),
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const sqrtPriceX64A = SqrtPriceMath.getSqrtPriceX64FromTick(positionTickLowerIndex);
const sqrtPriceX64B = SqrtPriceMath.getSqrtPriceX64FromTick(positionTickUpperIndex);
const { amountSlippageA: poolLiquidityA, amountSlippageB: poolLiquidityB } =
LiquidityMath.getAmountsFromLiquidityWithSlippage(
sqrtPriceX64,
sqrtPriceX64A,
sqrtPriceX64B,
poolLiquidity,
false,
false,
0,
);
const { amountSlippageA: userLiquidityA, amountSlippageB: userLiquidityB } =
LiquidityMath.getAmountsFromLiquidityWithSlippage(
sqrtPriceX64,
sqrtPriceX64A,
sqrtPriceX64B,
liquidity,
false,
false,
0,
);
const poolTvl = new Decimal(poolLiquidityA.toString())
.div(new Decimal(10).pow(mintDecimalsA))
.mul(mintPriceA.value)
.add(new Decimal(poolLiquidityB.toString()).div(new Decimal(10).pow(mintDecimalsB)).mul(mintPriceB.value));
const userTvl = new Decimal(userLiquidityA.toString())
.div(new Decimal(10).pow(mintDecimalsA))
.mul(mintPriceA.value)
.add(new Decimal(userLiquidityB.toString()).div(new Decimal(10).pow(mintDecimalsB)).mul(mintPriceB.value));
const p = new Decimal(1).div(poolTvl.add(userTvl));
const feesPerYear = new Decimal(aprInfo.volumeFee).mul(365).div(aprTypeDay);
const feeApr = feesPerYear.mul(p).mul(100).toNumber();
const SECONDS_PER_YEAR = 3600 * 24 * 365;
const rewardsApr = poolInfo.rewardDefaultInfos.map((i) => {
const iDecimal = i.mint.decimals;
const iPrice = mintPrice[i.mint.address];
if (
chainTime < ((i as any).startTime ?? 0) ||
chainTime > ((i as any).endTime ?? 0) ||
!i.perSecond ||
!iPrice ||
iDecimal === undefined
)
return 0;
return new Decimal(iPrice.value)
.mul(new Decimal(i.perSecond).mul(SECONDS_PER_YEAR))
.div(new Decimal(10).pow(iDecimal))
.mul(p)
.mul(100)
.toNumber();
});
return {
feeApr,
rewardsApr,
apr: feeApr + rewardsApr.reduce((a, b) => a + b, 0),
};
}
static async getLiquidityAmountOutFromAmountIn({
poolInfo,
inputA,
tickLower,
tickUpper,
amount,
slippage,
add,
epochInfo,
amountHasFee,
}: {
poolInfo: ApiV3PoolInfoConcentratedItem;
inputA: boolean;
tickLower: number;
tickUpper: number;
amount: BN;
slippage: number;
add: boolean;
epochInfo: EpochInfo;
amountHasFee: boolean;
}): Promise<ReturnTypeGetLiquidityAmountOut> {
const sqrtPriceX64 = SqrtPriceMath.priceToSqrtPriceX64(
new Decimal(poolInfo.price),
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const sqrtPriceX64A = SqrtPriceMath.getSqrtPriceX64FromTick(tickLower);
const sqrtPriceX64B = SqrtPriceMath.getSqrtPriceX64FromTick(tickUpper);
// const coefficient = add ? 1 - slippage : 1 + slippage;
const addFeeAmount = getTransferAmountFeeV2(
amount,
poolInfo[inputA ? "mintA" : "mintB"].extensions?.feeConfig,
epochInfo,
!amountHasFee,
);
const _amount = new BN(
new Decimal(addFeeAmount.amount.sub(addFeeAmount.fee ?? ZERO).toString()).toFixed(0), // .mul(coefficient).toFixed(0),
);
let liquidity: BN;
if (sqrtPriceX64.lte(sqrtPriceX64A)) {
liquidity = inputA
? LiquidityMath.getLiquidityFromTokenAmountA(sqrtPriceX64A, sqrtPriceX64B, _amount, !add)
: new BN(0);
} else if (sqrtPriceX64.lte(sqrtPriceX64B)) {
const liquidity0 = LiquidityMath.getLiquidityFromTokenAmountA(sqrtPriceX64, sqrtPriceX64B, _amount, !add);
const liquidity1 = LiquidityMath.getLiquidityFromTokenAmountB(sqrtPriceX64A, sqrtPriceX64, _amount);
liquidity = inputA ? liquidity0 : liquidity1;
} else {
liquidity = inputA
? new BN(0)
: LiquidityMath.getLiquidityFromTokenAmountB(sqrtPriceX64A, sqrtPriceX64B, _amount);
}
const amountFromLiquidity = await PoolUtils.getAmountsFromLiquidity({
epochInfo,
poolInfo,
tickLower,
tickUpper,
liquidity,
slippage,
add,
});
return {
liquidity,
amountA: inputA ? addFeeAmount : amountFromLiquidity.amountA,
amountB: inputA ? amountFromLiquidity.amountB : addFeeAmount,
amountSlippageA: inputA ? addFeeAmount : amountFromLiquidity.amountSlippageA,
amountSlippageB: inputA ? amountFromLiquidity.amountSlippageB : addFeeAmount,
expirationTime: amountFromLiquidity.expirationTime,
};
}
static async getAmountsFromLiquidity({
epochInfo,
poolInfo,
tickLower,
tickUpper,
liquidity,
slippage,
add,
}: {
epochInfo: EpochInfo;
poolInfo: ApiV3PoolInfoConcentratedItem;
tickLower: number;
tickUpper: number;
liquidity: BN;
slippage: number;
add: boolean;
}): Promise<ReturnTypeGetLiquidityAmountOut> {
const sqrtPriceX64A = SqrtPriceMath.getSqrtPriceX64FromTick(tickLower);
const sqrtPriceX64B = SqrtPriceMath.getSqrtPriceX64FromTick(tickUpper);
const coefficientRe = add ? 1 + slippage : 1 - slippage;
const amounts = LiquidityMath.getAmountsFromLiquidity(
SqrtPriceMath.priceToSqrtPriceX64(new Decimal(poolInfo.price), poolInfo.mintA.decimals, poolInfo.mintB.decimals),
sqrtPriceX64A,
sqrtPriceX64B,
liquidity,
add,
);
const [amountA, amountB] = [
getTransferAmountFeeV2(amounts.amountA, poolInfo.mintA.extensions?.feeConfig, epochInfo, true),
getTransferAmountFeeV2(amounts.amountB, poolInfo.mintB.extensions?.feeConfig, epochInfo, true),
];
const [amountSlippageA, amountSlippageB] = [
getTransferAmountFeeV2(
amounts.amountA.muln(coefficientRe),
poolInfo.mintA.extensions?.feeConfig,
epochInfo,
true,
),
getTransferAmountFeeV2(
amounts.amountB.muln(coefficientRe),
poolInfo.mintB.extensions?.feeConfig,
epochInfo,
true,
),
];
return {
liquidity,
amountA,
amountB,
amountSlippageA,
amountSlippageB,
expirationTime: minExpirationTime(amountA.expirationTime, amountB.expirationTime),
};
}
static async fetchComputeMultipleClmmInfo({
connection,
poolList,
rpcDataMap = {},
}: {
rpcDataMap?: Record<string, ReturnType<typeof PoolInfoLayout.decode>>;
connection: Connection;
poolList: Pick<ApiV3PoolInfoConcentratedItem, "id" | "programId" | "mintA" | "mintB" | "config" | "price">[];
}): Promise<Record<string, ComputeClmmPoolInfo>> {
const fetchRpcList = poolList.filter((p) => !rpcDataMap[p.id]).map((p) => new PublicKey(p.id));
const rpcRes = await getMultipleAccountsInfo(connection, fetchRpcList);
rpcRes.forEach((r, idx) => {
if (!r) return;
rpcDataMap[fetchRpcList[idx].toBase58()] = PoolInfoLayout.decode(r.data);
});
const pdaList = poolList.map(
(poolInfo) => getPdaExBitmapAccount(new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)).publicKey,
);
const exBitData = await PoolUtils.fetchExBitmaps({
connection,
exBitmapAddress: pdaList,
batchRequest: false,
});
return poolList.reduce(
(acc, cur) => ({
...acc,
[cur.id]: {
...rpcDataMap[cur.id],
id: new PublicKey(cur.id),
version: 6,
programId: new PublicKey(cur.programId),
mintA: cur.mintA,
mintB: cur.mintB,
ammConfig: {
...cur.config,
id: new PublicKey(cur.config.id),
fundOwner: "",
},
currentPrice: new Decimal(cur.price),
exBitmapAccount: getPdaExBitmapAccount(new PublicKey(cur.programId), new PublicKey(cur.id)).publicKey,
exBitmapInfo:
exBitData[getPdaExBitmapAccount(new PublicKey(cur.programId), new PublicKey(cur.id)).publicKey.toBase58()],
startTime: rpcDataMap[cur.id].startTime.toNumber(),
rewardInfos: rpcDataMap[cur.id].rewardInfos,
},
}),
{} as Record<string, ComputeClmmPoolInfo>,
);
}
static async fetchComputeClmmInfo({
connection,
poolInfo,
rpcData,
}: {
connection: Connection;
poolInfo: Pick<ApiV3PoolInfoConcentratedItem, "id" | "programId" | "mintA" | "mintB" | "config" | "price">;
rpcData?: ReturnType<typeof PoolInfoLayout.decode>;
}): Promise<ComputeClmmPoolInfo> {
return (
await this.fetchComputeMultipleClmmInfo({
connection,
rpcDataMap: rpcData ? { [poolInfo.id]: rpcData } : undefined,
poolList: [poolInfo],
})
)[poolInfo.id];
}
}
export function getLiquidityFromAmounts({
poolInfo,
tickLower,
tickUpper,
amountA,
amountB,
slippage,
add,
epochInfo,
amountHasFee,
}: {
poolInfo: ApiV3PoolInfoConcentratedItem;
tickLower: number;
tickUpper: number;
amountA: BN;
amountB: BN;
slippage: number;
add: boolean;
epochInfo: EpochInfo;
amountHasFee: boolean;
}): ReturnTypeGetLiquidityAmountOut {
const [_tickLower, _tickUpper, _amountA, _amountB] =
tickLower < tickUpper ? [tickLower, tickUpper, amountA, amountB] : [tickUpper, tickLower, amountB, amountA];
const sqrtPriceX64 = SqrtPriceMath.priceToSqrtPriceX64(
new Decimal(poolInfo.price),
poolInfo.mintA.decimals,
poolInfo.mintB.decimals,
);
const sqrtPriceX64A = SqrtPriceMath.getSqrtPriceX64FromTick(_tickLower);
const sqrtPriceX64B = SqrtPriceMath.getSqrtPriceX64FromTick(_tickUpper);
const [amountFeeA, amountFeeB] = [
getTransferAmountFeeV2(_amountA, poolInfo.mintA.extensions?.feeConfig, epochInfo, !amountHasFee),
getTransferAmountFeeV2(_amountB, poolInfo.mintB.extensions?.feeConfig, epochInfo, !amountHasFee),
];
const liquidity = LiquidityMath.getLiquidityFromTokenAmounts(
sqrtPriceX64,
sqrtPriceX64A,
sqrtPriceX64B,
amountFeeA.amount.sub(amountFeeA.fee ?? ZERO),
amountFeeB.amount.sub(amountFeeB.fee ?? ZERO),
);
return LiquidityMath.getAmountsOutFromLiquidity({
poolInfo,
tickLower,
tickUpper,
liquidity,
slippage,
add,
epochInfo,
amountAddFee: !amountHasFee,
});
}
const mockRewardData = {
volume: 0,
volumeQuote: 0,
volumeFee: 0,
apr: 0,
feeApr: 0,
priceMin: 0,
priceMax: 0,
rewardApr: [],
};
export function clmmComputeInfoToApiInfo(pool: ComputeClmmPoolInfo): ApiV3PoolInfoConcentratedItem {
return {
...pool,
type: "Concentrated",
programId: pool.programId.toString(),
id: pool.id.toString(),
rewardDefaultInfos: [],
rewardDefaultPoolInfos: "Clmm",
price: pool.currentPrice.toNumber(),
mintAmountA: 0,
mintAmountB: 0,
feeRate: pool.ammConfig.tradeFeeRate,
openTime: pool.startTime.toString(),
tvl: 0,
day: mockRewardData,
week: mockRewardData,
month: mockRewardData,
pooltype: [],
farmUpcomingCount: 0,
farmOngoingCount: 0,
farmFinishedCount: 0,
burnPercent: 0,
config: {
...pool.ammConfig,
id: pool.ammConfig.id.toString(),
defaultRange: 0,
defaultRangePoint: [],
},
};
}