@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
392 lines (339 loc) • 12 kB
text/typescript
import { BN } from '@coral-xyz/anchor';
import {
address,
Address,
GetAccountInfoApi,
getAddressEncoder,
GetMultipleAccountsApi,
getProgramDerivedAddress,
Rpc,
} from '@solana/kit';
import { ProgramDerivedAddress } from '@solana/addresses/dist/types/program-derived-address';
import { ONE_BN, U64_MAX, ZERO_BN } from '../constants';
import { Percentage } from './types';
import { DEFAULT_ADDRESS, IncreaseLiquidityQuoteParam } from '@orca-so/whirlpools';
import { TickArray, Whirlpool } from '../@codegen/whirlpools/accounts';
import {
_MAX_TICK_INDEX,
_MIN_TICK_INDEX,
getTickArrayStartTickIndex,
IncreaseLiquidityQuote,
tickIndexToPrice,
TransferFee,
} from '@orca-so/whirlpools-core';
import { increaseLiquidityQuoteA, increaseLiquidityQuoteB, increaseLiquidityQuote } from '@orca-so/whirlpools-core';
import { Whirlpool as WhirlpoolAPIResponse, WhirlpoolReward } from '../services/OrcaWhirlpoolsResponse';
import Decimal from 'decimal.js/decimal';
import { getRemoveLiquidityQuote } from './whirlpools';
import { FullBPS } from './CreationParameters';
import { fetchMaybeTickArray, getTickArrayAddress } from '@orca-so/whirlpools-client';
import { sleep } from './utils';
import { ZERO } from './math';
export const defaultSlippagePercentageBPS = 10;
const addressEncoder = getAddressEncoder();
const TICK_ARRAY_SIZE = 88;
const SECONDS_PER_YEAR =
60 * // SECONDS
60 * // MINUTES
24 * // HOURS
365; // DAYS
function estimateRewardApr(
reward: WhirlpoolReward,
rewardTokenDecimals: number,
concentratedValue: Decimal,
tokenPrices: Map<Address, Decimal>
) {
const { mint } = reward;
const rewardTokenPrice = tokenPrices.get(address(mint));
const emissionsPerSecond = new Decimal(reward.emissionsPerSecond).div(new Decimal(10).pow(rewardTokenDecimals));
if (emissionsPerSecond.eq(ZERO) || !rewardTokenPrice) {
return 0;
}
const res = emissionsPerSecond.mul(SECONDS_PER_YEAR).mul(rewardTokenPrice).div(concentratedValue).toNumber();
return res;
}
export async function getTickArray(
programId: Address,
whirlpoolAddress: Address,
startTick: number
): Promise<ProgramDerivedAddress> {
return await getProgramDerivedAddress({
seeds: [Buffer.from('tick_array'), addressEncoder.encode(whirlpoolAddress), Buffer.from(startTick.toString())],
programAddress: programId,
});
}
export function getTokenAFromLiquidity(liquidity: BN, sqrtPrice0X64: BN, sqrtPrice1X64: BN, roundUp: boolean) {
const [sqrtPriceLowerX64, sqrtPriceUpperX64] = orderSqrtPrice(sqrtPrice0X64, sqrtPrice1X64);
const numerator = liquidity.mul(sqrtPriceUpperX64.sub(sqrtPriceLowerX64)).shln(64);
const denominator = sqrtPriceUpperX64.mul(sqrtPriceLowerX64);
if (roundUp) {
return divRoundUp(numerator, denominator);
} else {
return numerator.div(denominator);
}
}
export function getTokenBFromLiquidity(liquidity: BN, sqrtPrice0X64: BN, sqrtPrice1X64: BN, roundUp: boolean) {
const [sqrtPriceLowerX64, sqrtPriceUpperX64] = orderSqrtPrice(sqrtPrice0X64, sqrtPrice1X64);
const result = liquidity.mul(sqrtPriceUpperX64.sub(sqrtPriceLowerX64));
if (roundUp) {
return shiftRightRoundUp(result);
} else {
return result.shrn(64);
}
}
export function getNearestValidTickIndexFromTickIndex(tickIndex: number, tickSpacing: number): number {
return tickIndex - (tickIndex % tickSpacing);
}
export function getIncreaseLiquidityQuote(
param: IncreaseLiquidityQuoteParam,
pool: Whirlpool,
tickLowerIndex: number,
tickUpperIndex: number,
slippageToleranceBps: number,
transferFeeA: TransferFee | undefined,
transferFeeB: TransferFee | undefined
): IncreaseLiquidityQuote {
if ('liquidity' in param) {
return increaseLiquidityQuote(
param.liquidity,
slippageToleranceBps,
BigInt(pool.sqrtPrice.toString()),
tickLowerIndex,
tickUpperIndex,
transferFeeA,
transferFeeB
);
} else if ('tokenA' in param) {
return increaseLiquidityQuoteA(
param.tokenA,
slippageToleranceBps,
BigInt(pool.sqrtPrice.toString()),
tickLowerIndex,
tickUpperIndex,
transferFeeA,
transferFeeB
);
} else {
return increaseLiquidityQuoteB(
param.tokenB,
slippageToleranceBps,
BigInt(pool.sqrtPrice.toString()),
tickLowerIndex,
tickUpperIndex,
transferFeeA,
transferFeeB
);
}
}
function orderSqrtPrice(sqrtPrice0X64: BN, sqrtPrice1X64: BN): [BN, BN] {
if (sqrtPrice0X64.lt(sqrtPrice1X64)) {
return [sqrtPrice0X64, sqrtPrice1X64];
} else {
return [sqrtPrice1X64, sqrtPrice0X64];
}
}
function shiftRightRoundUp(n: BN): BN {
let result = n.shrn(64);
if (n.mod(new BN(U64_MAX)).gt(ZERO_BN)) {
result = result.add(ONE_BN);
}
return result;
}
function divRoundUp(n0: BN, n1: BN): BN {
const hasRemainder = !n0.mod(n1).eq(ZERO_BN);
if (hasRemainder) {
return n0.div(n1).add(ONE_BN);
} else {
return n0.div(n1);
}
}
export function adjustForSlippage(n: BN, { numerator, denominator }: Percentage, adjustUp: boolean): BN {
if (adjustUp) {
return n.mul(denominator.add(numerator)).div(denominator);
} else {
return n.mul(denominator).div(denominator.add(numerator));
}
}
export type EstimatedAprs = {
fee: number;
rewards: number[];
};
export const ZERO_APR = {
fee: 0,
rewards: [0, 0, 0],
};
export function estimateAprsForPriceRange(
pool: WhirlpoolAPIResponse,
tokenPrices: Map<Address, Decimal>,
fees24h: number,
tickLowerIndex: number,
tickUpperIndex: number,
rewardsDecimals: Map<Address, number>
): EstimatedAprs {
const tokenPriceA = tokenPrices.get(address(pool.tokenMintA));
const tokenPriceB = tokenPrices.get(address(pool.tokenMintB));
if (!fees24h || !tokenPriceA || !tokenPriceB || tickLowerIndex >= tickUpperIndex) {
return ZERO_APR;
}
// Value of liquidity if the entire liquidity were concentrated between tickLower/Upper
// Since this is virtual liquidity, concentratedValue should actually be less than totalValue
const { minTokenA, minTokenB } = getRemoveLiquidityQuote({
positionAddress: DEFAULT_ADDRESS,
tickCurrentIndex: pool.tickCurrentIndex,
sqrtPrice: new BN(pool.sqrtPrice.toString()),
tickLowerIndex,
tickUpperIndex,
liquidity: new BN(pool.liquidity.toString()),
slippageTolerance: { numerator: ZERO_BN, denominator: new BN(FullBPS) },
});
const tokenValueA = getTokenValue(minTokenA, pool.tokenA.decimals, tokenPriceA);
const tokenValueB = getTokenValue(minTokenB, pool.tokenB.decimals, tokenPriceB);
const concentratedValue = tokenValueA.add(tokenValueB);
const feesPerYear = new Decimal(fees24h).mul(365).div(new Decimal(10).pow(6)); // scale from lamports of USDC to tokens
const feeApr = feesPerYear.div(concentratedValue).toNumber();
const rewards = pool.rewards.map((reward) => {
if (rewardsDecimals.has(address(reward.mint))) {
return estimateRewardApr(reward, rewardsDecimals.get(address(reward.mint))!, concentratedValue, tokenPrices);
} else {
return 0;
}
});
return { fee: feeApr, rewards };
}
function getTokenValue(tokenAmount: BN, decimals: number, tokenPrice: Decimal): Decimal {
return tokenPrice.mul(new Decimal(tokenAmount.toString()).div(new Decimal(10).pow(decimals)));
}
export async function getLowestInitializedTickArrayTickIndex(
rpc: Rpc<GetAccountInfoApi>,
whirlpoolAddress: Address,
tickSpacing: number
): Promise<number> {
const minTick = _MIN_TICK_INDEX();
let startTickIndex = getTickArrayStartTickIndex(minTick, tickSpacing);
// eslint-disable-next-line
while (true) {
const [tickArrayAddress] = await getTickArrayAddress(whirlpoolAddress, startTickIndex);
const tickArray = await fetchMaybeTickArray(rpc, tickArrayAddress);
if (tickArray.exists) {
return startTickIndex;
}
startTickIndex += TICK_ARRAY_SIZE * tickSpacing;
await sleep(500);
}
}
export async function getHighestInitializedTickArrayTickIndex(
rpc: Rpc<GetAccountInfoApi>,
whirlpoolAddress: Address,
tickSpacing: number
): Promise<number> {
const maxTick = _MAX_TICK_INDEX();
let startTickIndex = getTickArrayStartTickIndex(maxTick, tickSpacing);
// eslint-disable-next-line
while (true) {
const [tickArrayAddress] = await getTickArrayAddress(whirlpoolAddress, startTickIndex);
const tickArray = await fetchMaybeTickArray(rpc, tickArrayAddress);
if (tickArray.exists) {
return startTickIndex;
}
startTickIndex -= TICK_ARRAY_SIZE * tickSpacing;
await sleep(500);
}
}
export type LiquidityDataPoint = {
liquidity: Decimal;
price: Decimal;
tickIndex: number;
};
export type WhirlpoolLiquidityDistribution = {
currentPrice: Decimal;
currentTickIndex: number;
datapoints: LiquidityDataPoint[];
};
export async function getLiquidityDistribution(
rpc: Rpc<GetMultipleAccountsApi>,
poolAddress: Address,
poolData: WhirlpoolAPIResponse,
tickLower: number,
tickUpper: number,
whirlpoolProgramId: Address
): Promise<WhirlpoolLiquidityDistribution> {
const datapoints: LiquidityDataPoint[] = [];
const tokenDecimalsA = poolData.tokenA.decimals;
const tokenDecimalsB = poolData.tokenB.decimals;
const tickArrayAddresses = await getSurroundingTickArrayAddresses(
poolAddress,
poolData,
tickLower,
tickUpper,
whirlpoolProgramId
);
const tickArrays = await TickArray.fetchMultiple(rpc, tickArrayAddresses, whirlpoolProgramId);
const currentLiquidity = new Decimal(poolData.liquidity.toString());
let relativeLiquidity = currentLiquidity;
let minLiquidity = new Decimal(0);
let liquidity = new Decimal(0);
tickArrays.forEach((tickArray) => {
if (!tickArray) {
return;
}
const startIndex = tickArray.startTickIndex;
tickArray.ticks.forEach((tick, index) => {
const tickIndex = startIndex + index * poolData.tickSpacing;
const price = tickIndexToPrice(tickIndex, tokenDecimalsA, tokenDecimalsB);
const liquidityNet = new Decimal(tick.liquidityNet.toString());
liquidity = liquidity.add(liquidityNet);
datapoints.push({ liquidity: new Decimal(liquidity), price: new Decimal(price), tickIndex });
minLiquidity = liquidity.lt(minLiquidity) ? liquidity : minLiquidity;
if (tickIndex === poolData.tickCurrentIndex) {
relativeLiquidity = relativeLiquidity.sub(liquidityNet);
}
});
});
if (!relativeLiquidity.eq(currentLiquidity)) {
minLiquidity = minLiquidity.add(relativeLiquidity);
datapoints.forEach((datapoint) => {
datapoint.liquidity = datapoint.liquidity.add(relativeLiquidity);
});
}
if (minLiquidity.lt(0)) {
datapoints.forEach((datapoint) => {
datapoint.liquidity = datapoint.liquidity.add(minLiquidity.neg());
});
}
return {
currentPrice: new Decimal(poolData.price),
currentTickIndex: poolData.tickCurrentIndex,
datapoints,
};
}
async function getSurroundingTickArrayAddresses(
poolAddress: Address,
pool: WhirlpoolAPIResponse,
tickLower: number,
tickUpper: number,
programId: Address
): Promise<Address[]> {
const tickArrayAddresses: Address[] = [];
let startIndex = getTickArrayStartTickIndex(tickLower, pool.tickSpacing);
while (startIndex <= tickUpper) {
const [address, _bump] = await getTickArrayPda(programId, poolAddress, startIndex);
tickArrayAddresses.push(address);
try {
startIndex = getTickArrayStartTickIndex(startIndex, pool.tickSpacing);
} catch (_e) {
return tickArrayAddresses;
}
}
return tickArrayAddresses;
}
export async function getTickArrayPda(
programId: Address,
poolAddress: Address,
startIndex: number
): Promise<[Address, number]> {
const pdaWithBump = await getProgramDerivedAddress({
seeds: [Buffer.from('tick_array'), addressEncoder.encode(poolAddress), Buffer.from(startIndex.toString())],
programAddress: programId,
});
return [pdaWithBump[0], pdaWithBump[1]];
}