@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
318 lines • 15.5 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrcaService = void 0;
const kit_1 = require("@solana/kit");
const decimal_js_1 = __importDefault(require("decimal.js"));
const axios_1 = __importDefault(require("axios"));
const accounts_1 = require("../@codegen/whirlpools/accounts");
const utils_1 = require("../utils");
const programId_1 = require("../@codegen/whirlpools/programId");
const whirlpools_core_1 = require("@orca-so/whirlpools-core");
class OrcaService {
_rpc;
_whirlpoolProgramId;
_orcaApiUrl;
constructor(rpc, legacyConnection, whirlpoolProgramId = programId_1.PROGRAM_ID) {
this._rpc = rpc;
this._whirlpoolProgramId = whirlpoolProgramId;
this._orcaApiUrl = `https://api.orca.so/v2/solana`;
}
getWhirlpoolProgramId() {
return this._whirlpoolProgramId;
}
// Fetch all Orca whirlpools with pagination support (note there are over 20 pages so it may take a while)
async getOrcaWhirlpools(tokens = []) {
const maxPageSize = 1000;
const maxPages = 100; // Safety limit to prevent infinite loops
const allWhirlpools = [];
let after = undefined;
let hasMore = true;
let pageCount = 0;
while (hasMore && pageCount < maxPages) {
pageCount++;
const url = new URL(`${this._orcaApiUrl}/pools`);
url.searchParams.set('size', maxPageSize.toString());
if (after) {
url.searchParams.set('after', after);
}
// Add token filtering parameters based on the number of tokens provided
if (tokens.length === 1) {
url.searchParams.set('token', tokens[0]);
}
else if (tokens.length === 2) {
url.searchParams.set('tokensBothOf', tokens.join(','));
}
try {
const response = await axios_1.default.get(url.toString());
const data = response.data;
// Add whirlpools from this page to our collection
if (data.data && data.data.length > 0) {
allWhirlpools.push(...data.data);
}
// Check if there are more pages using the meta.cursor.next field
if (data.meta?.cursor?.next) {
after = data.meta.cursor.next;
hasMore = true;
}
else {
hasMore = false;
}
}
catch (error) {
console.error('Error fetching Orca whirlpools page:', error);
throw error;
}
}
if (pageCount >= maxPages) {
console.warn(`Reached maximum page limit (${maxPages}). There might be more whirlpools available.`);
}
return allWhirlpools;
}
async getOrcaWhirlpool(poolAddress) {
const response = await axios_1.default.get(`${this._orcaApiUrl}/pools/${poolAddress}`);
// If the API response has a nested data field that contains the actual pool data
if (response.data.data && typeof response.data.data === 'object') {
return response.data.data;
}
return response.data;
}
/**
* Get token prices for a strategy - for use with orca sdk
* @param strategy
* @param prices
* @param collateralInfos
* @returns {Record<string, Decimal>} - token prices by mint string
* @private
*/
getTokenPrices(strategy, prices, collateralInfos) {
const tokensPrices = new Map();
const tokenA = collateralInfos[strategy.tokenACollateralId.toNumber()];
const tokenB = collateralInfos[strategy.tokenBCollateralId.toNumber()];
const rewardToken0 = collateralInfos[strategy.reward0CollateralId.toNumber()];
const rewardToken1 = collateralInfos[strategy.reward1CollateralId.toNumber()];
const rewardToken2 = collateralInfos[strategy.reward2CollateralId.toNumber()];
const aPrice = prices.spot[tokenA.mint.toString()];
const bPrice = prices.spot[tokenB.mint.toString()];
const reward0Price = strategy.reward0Decimals.toNumber() !== 0 ? prices.spot[rewardToken0.mint.toString()] : null;
const reward1Price = strategy.reward1Decimals.toNumber() !== 0 ? prices.spot[rewardToken1.mint.toString()] : null;
const reward2Price = strategy.reward2Decimals.toNumber() !== 0 ? prices.spot[rewardToken2.mint.toString()] : null;
const [mintA, mintB] = [(0, kit_1.address)(strategy.tokenAMint.toString()), (0, kit_1.address)(strategy.tokenBMint.toString())];
const reward0 = (0, kit_1.address)(collateralInfos[strategy.reward0CollateralId.toNumber()]?.mint?.toString());
const reward1 = (0, kit_1.address)(collateralInfos[strategy.reward1CollateralId.toNumber()]?.mint?.toString());
const reward2 = (0, kit_1.address)(collateralInfos[strategy.reward2CollateralId.toNumber()]?.mint?.toString());
tokensPrices.set(mintA, aPrice.price);
tokensPrices.set(mintB, bPrice.price);
if (reward0Price !== null) {
tokensPrices.set(reward0, reward0Price.price);
}
if (reward1Price !== null) {
tokensPrices.set(reward1, reward1Price.price);
}
if (reward2Price !== null) {
tokensPrices.set(reward2, reward2Price.price);
}
return tokensPrices;
}
getPoolTokensPrices(pool, prices) {
const tokensPrices = new Map();
const tokens = [
(0, kit_1.address)(pool.tokenMintA.toString()),
(0, kit_1.address)(pool.tokenMintB.toString()),
(0, kit_1.address)(pool.rewards[0]?.mint.toString()),
(0, kit_1.address)(pool.rewards[1]?.mint.toString()),
(0, kit_1.address)(pool.rewards[2]?.mint.toString()),
];
for (const mint of tokens) {
if (mint) {
const price = prices.spot[mint]?.price;
if (!price) {
throw new Error(`Could not get token ${mint} price`);
}
tokensPrices.set(mint, price);
}
}
return tokensPrices;
}
async getStrategyWhirlpoolPoolAprApy(strategy, collateralInfos, prices) {
const position = await accounts_1.Position.fetch(this._rpc, strategy.position);
if (!position) {
throw new Error(`Position ${strategy.position.toString()} does not exist`);
}
const pool = await this.getOrcaWhirlpool(strategy.pool);
if (!pool) {
throw Error(`Could not get orca pool data for ${strategy.pool.toString()}`);
}
const priceRange = (0, utils_1.getStrategyPriceRangeOrca)(position.tickLowerIndex, position.tickUpperIndex, strategy, new decimal_js_1.default(pool.price.toString()));
if (priceRange.strategyOutOfRange) {
return {
priceLower: new decimal_js_1.default(priceRange.priceLower),
priceUpper: new decimal_js_1.default(priceRange.priceUpper),
poolPrice: new decimal_js_1.default(pool.price),
strategyOutOfRange: priceRange.strategyOutOfRange,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const lpFeeRate = new decimal_js_1.default(pool.feeRate);
const volume24hUsd = pool.stats['24h']?.volume ?? new decimal_js_1.default(0);
const fee24Usd = new decimal_js_1.default(volume24hUsd).mul(lpFeeRate).toNumber();
const tokensPrices = this.getTokenPrices(strategy, prices, collateralInfos);
const rewardsDecimals = new Map();
if (strategy.reward0Decimals.toNumber() !== 0) {
rewardsDecimals.set((0, kit_1.address)(pool.rewards[0]?.mint), strategy.reward0Decimals.toNumber());
}
if (strategy.reward1Decimals.toNumber() !== 0) {
rewardsDecimals.set((0, kit_1.address)(pool.rewards[1]?.mint), strategy.reward1Decimals.toNumber());
}
if (strategy.reward2Decimals.toNumber() !== 0) {
rewardsDecimals.set((0, kit_1.address)(pool.rewards[2]?.mint), strategy.reward2Decimals.toNumber());
}
const apr = (0, utils_1.estimateAprsForPriceRange)(pool, tokensPrices, fee24Usd, position.tickLowerIndex, position.tickUpperIndex, rewardsDecimals);
let totalApr = new decimal_js_1.default(apr.fee);
for (const reward of apr.rewards) {
totalApr = totalApr.add(reward);
}
const feeApr = new decimal_js_1.default(apr.fee);
const rewardsApr = apr.rewards.map((r) => new decimal_js_1.default(r));
return {
totalApr,
totalApy: (0, utils_1.aprToApy)(totalApr, 365),
feeApr,
feeApy: (0, utils_1.aprToApy)(feeApr, 365),
rewardsApr,
rewardsApy: rewardsApr.map((x) => (0, utils_1.aprToApy)(x, 365)),
priceLower: new decimal_js_1.default(priceRange.priceLower),
priceUpper: new decimal_js_1.default(priceRange.priceUpper),
poolPrice: new decimal_js_1.default(pool.price),
strategyOutOfRange: priceRange.strategyOutOfRange,
};
}
// strongly recommended to pass lowestTick and highestTick because fetching the lowest and highest existent takes very long
async getWhirlpoolLiquidityDistribution(pool, keepOrder = true, lowestTick, highestTick) {
const whirlpool = await this.getOrcaWhirlpool(pool);
if (!whirlpool) {
throw new Error(`Could not get pool data for Whirlpool ${pool}`);
}
let lowestInitializedTick;
if (lowestTick) {
lowestInitializedTick = lowestTick;
}
else {
lowestInitializedTick = await (0, utils_1.getLowestInitializedTickArrayTickIndex)(this._rpc, pool, whirlpool.tickSpacing);
}
let highestInitializedTick;
if (highestTick) {
highestInitializedTick = highestTick;
}
else {
highestInitializedTick = await (0, utils_1.getHighestInitializedTickArrayTickIndex)(this._rpc, pool, whirlpool.tickSpacing);
}
const orcaLiqDistribution = await (0, utils_1.getLiquidityDistribution)(this._rpc, pool, whirlpool, lowestInitializedTick, highestInitializedTick, this._whirlpoolProgramId);
const liqDistribution = {
currentPrice: new decimal_js_1.default(whirlpool.price),
currentTickIndex: whirlpool.tickCurrentIndex,
distribution: [],
};
orcaLiqDistribution.datapoints.forEach((entry) => {
let priceWithOrder = new decimal_js_1.default(entry.price);
if (!keepOrder) {
priceWithOrder = new decimal_js_1.default(1).div(priceWithOrder);
}
const liq = {
price: priceWithOrder,
liquidity: entry.liquidity,
tickIndex: entry.tickIndex,
};
liqDistribution.distribution.push(liq);
});
return liqDistribution;
}
async getWhirlpoolPositionAprApy(poolPubkey, priceLower, priceUpper, prices, rewardsDecimals) {
const pool = await this.getOrcaWhirlpool(poolPubkey);
if (!pool) {
throw Error(`Could not get orca pool data for ${poolPubkey}`);
}
let strategyOutOfRange = false;
if (priceLower.gt(new decimal_js_1.default(pool.price)) || priceUpper.lt(new decimal_js_1.default(pool.price))) {
strategyOutOfRange = true;
}
if (strategyOutOfRange) {
return {
priceLower,
priceUpper,
strategyOutOfRange,
poolPrice: new decimal_js_1.default(pool.price),
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const lpFeeRate = pool.feeRate;
const volume24hUsd = pool?.stats?.['24h']?.volume ?? new decimal_js_1.default(0);
const fee24Usd = new decimal_js_1.default(volume24hUsd).mul(lpFeeRate).toNumber();
const tokensPrices = this.getPoolTokensPrices(pool, prices);
const tickLowerIndex = (0, utils_1.getNearestValidTickIndexFromTickIndex)((0, whirlpools_core_1.priceToTickIndex)(priceLower.toNumber(), pool.tokenA.decimals, pool.tokenB.decimals), pool.tickSpacing);
const tickUpperIndex = (0, utils_1.getNearestValidTickIndexFromTickIndex)((0, whirlpools_core_1.priceToTickIndex)(priceUpper.toNumber(), pool.tokenA.decimals, pool.tokenB.decimals), pool.tickSpacing);
const apr = (0, utils_1.estimateAprsForPriceRange)(pool, tokensPrices, fee24Usd, tickLowerIndex, tickUpperIndex, rewardsDecimals);
const totalApr = new decimal_js_1.default(apr.fee).add(apr.rewards[0]).add(apr.rewards[1]).add(apr.rewards[2]);
const feeApr = new decimal_js_1.default(apr.fee);
const rewardsApr = apr.rewards.map((r) => new decimal_js_1.default(r));
return {
totalApr,
totalApy: (0, utils_1.aprToApy)(totalApr, 365),
feeApr,
feeApy: (0, utils_1.aprToApy)(feeApr, 365),
rewardsApr,
rewardsApy: rewardsApr.map((x) => (0, utils_1.aprToApy)(x, 365)),
priceLower,
priceUpper,
poolPrice: new decimal_js_1.default(pool.price),
strategyOutOfRange,
};
}
async getGenericPoolInfo(poolPubkey) {
const pool = await this.getOrcaWhirlpool(poolPubkey);
if (!pool) {
throw Error(`Could not get orca pool data for ${poolPubkey.toString()}`);
}
const poolInfo = {
dex: 'ORCA',
address: poolPubkey,
tokenMintA: (0, kit_1.address)(pool.tokenMintA),
tokenMintB: (0, kit_1.address)(pool.tokenMintB),
price: new decimal_js_1.default(pool.price),
feeRate: new decimal_js_1.default(pool.feeRate),
volumeOnLast7d: pool.stats['7d'] ? new decimal_js_1.default(pool.stats['7d'].volume) : undefined,
tvl: pool.tvlUsdc ? new decimal_js_1.default(pool.tvlUsdc) : undefined,
tickSpacing: new decimal_js_1.default(pool.tickSpacing),
// todo(Silviu): get real amount of positions
positions: new decimal_js_1.default(0),
};
return poolInfo;
}
async getPositionsCountByPool(pool) {
const rawPositions = await this._rpc
.getProgramAccounts(programId_1.PROGRAM_ID, {
commitment: 'confirmed',
filters: [
// account LAYOUT: https://github.com/orca-so/whirlpools/blob/main/programs/whirlpool/src/state/position.rs#L20
{ dataSize: 216n },
{ memcmp: { bytes: pool.toString(), offset: 8n, encoding: 'base58' } },
],
})
.send();
return rawPositions.length;
}
}
exports.OrcaService = OrcaService;
//# sourceMappingURL=OrcaService.js.map
;