@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
286 lines • 13.4 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 decimal_js_1 = __importDefault(require("decimal.js"));
const whirlpool_sdk_1 = require("@orca-so/whirlpool-sdk");
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 compat_1 = require("@solana/compat");
class OrcaService {
_rpc;
_legacyConnection;
_whirlpoolProgramId;
_orcaNetwork;
_orcaApiUrl;
constructor(rpc, legacyConnection, cluster, whirlpoolProgramId = programId_1.PROGRAM_ID) {
this._rpc = rpc;
this._legacyConnection = legacyConnection;
this._whirlpoolProgramId = whirlpoolProgramId;
this._orcaNetwork = cluster === 'mainnet-beta' ? whirlpool_sdk_1.OrcaNetwork.MAINNET : whirlpool_sdk_1.OrcaNetwork.DEVNET;
this._orcaApiUrl = `https://api.${cluster === 'mainnet-beta' ? 'mainnet' : 'devnet'}.orca.so`;
}
getWhirlpoolProgramId() {
return this._whirlpoolProgramId;
}
async getOrcaWhirlpools() {
return (await axios_1.default.get(`${this._orcaApiUrl}/v1/whirlpool/list`)).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 = {};
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] = [strategy.tokenAMint.toString(), strategy.tokenBMint.toString()];
const reward0 = collateralInfos[strategy.reward0CollateralId.toNumber()]?.mint?.toString();
const reward1 = collateralInfos[strategy.reward1CollateralId.toNumber()]?.mint?.toString();
const reward2 = collateralInfos[strategy.reward2CollateralId.toNumber()]?.mint?.toString();
tokensPrices[mintA] = aPrice.price;
tokensPrices[mintB] = bPrice.price;
if (reward0Price !== null) {
tokensPrices[reward0] = reward0Price.price;
}
if (reward1Price !== null) {
tokensPrices[reward1] = reward1Price.price;
}
if (reward2Price !== null) {
tokensPrices[reward2] = reward2Price.price;
}
return tokensPrices;
}
getPoolTokensPrices(pool, prices) {
const tokensPrices = {};
const tokens = [
pool.tokenMintA.toString(),
pool.tokenMintB.toString(),
pool.rewards[0].mint.toString(),
pool.rewards[1].mint.toString(),
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[mint] = price;
}
}
return tokensPrices;
}
async getPool(poolAddress) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._legacyConnection,
network: this._orcaNetwork,
});
return orca.getPool(poolAddress);
}
async getStrategyWhirlpoolPoolAprApy(strategy, collateralInfos, prices, whirlpools) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._legacyConnection,
network: this._orcaNetwork,
});
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 orca.getPool(strategy.pool);
if (!whirlpools) {
({ whirlpools } = await this.getOrcaWhirlpools());
}
const whirlpool = whirlpools?.find((x) => x.address === strategy.pool);
if (!pool || !whirlpool) {
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 {
...priceRange,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const lpFeeRate = pool.feePercentage;
const volume24hUsd = whirlpool?.volume?.day ?? 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 apr = (0, whirlpool_sdk_1.estimateAprsForPriceRange)(pool, tokensPrices, fee24Usd, position.tickLowerIndex, position.tickUpperIndex);
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)),
...priceRange,
};
}
// 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 orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._legacyConnection,
network: this._orcaNetwork,
});
const poolData = await orca.getPool(pool);
if (!poolData) {
throw new Error(`Could not get pool data for Whirlpool ${pool}`);
}
let lowestInitializedTick;
if (lowestTick) {
lowestInitializedTick = lowestTick;
}
else {
lowestInitializedTick = await orca.pool.getLowestInitializedTickArrayTickIndex(pool, poolData.tickSpacing);
}
let highestInitializedTick;
if (highestTick) {
highestInitializedTick = highestTick;
}
else {
highestInitializedTick = await orca.pool.getHighestInitializedTickArrayTickIndex(pool, poolData.tickSpacing);
}
const orcaLiqDistribution = await orca.pool.getLiquidityDistribution(pool, lowestInitializedTick, highestInitializedTick);
const liqDistribution = {
currentPrice: poolData.price,
currentTickIndex: poolData.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, whirlpools) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._legacyConnection,
network: this._orcaNetwork,
});
const pool = await orca.getPool(poolPubkey);
if (!whirlpools) {
({ whirlpools } = await this.getOrcaWhirlpools());
}
const whirlpool = whirlpools?.find((x) => x.address === poolPubkey.toString());
if (!pool || !whirlpool) {
throw Error(`Could not get orca pool data for ${poolPubkey}`);
}
let strategyOutOfRange = false;
if (priceLower.gt(pool.price) || priceUpper.lt(pool.price)) {
strategyOutOfRange = true;
}
if (strategyOutOfRange) {
return {
priceLower,
priceUpper,
strategyOutOfRange,
poolPrice: pool.price,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const lpFeeRate = pool.feePercentage;
const volume24hUsd = whirlpool?.volume?.day ?? 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, whirlpool_sdk_1.getNearestValidTickIndexFromTickIndex)((0, whirlpool_sdk_1.priceToTickIndex)(priceLower, pool.tokenDecimalsA, pool.tokenDecimalsB), whirlpool.tickSpacing);
const tickUpperIndex = (0, whirlpool_sdk_1.getNearestValidTickIndexFromTickIndex)((0, whirlpool_sdk_1.priceToTickIndex)(priceUpper, pool.tokenDecimalsA, pool.tokenDecimalsB), whirlpool.tickSpacing);
const apr = (0, whirlpool_sdk_1.estimateAprsForPriceRange)(pool, tokensPrices, fee24Usd, tickLowerIndex, tickUpperIndex);
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: pool.price,
strategyOutOfRange,
};
}
async getGenericPoolInfo(poolPubkey, whirlpools) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._legacyConnection,
network: this._orcaNetwork,
});
const poolString = poolPubkey.toString();
const pool = await orca.getPool(poolPubkey);
if (!whirlpools) {
({ whirlpools } = await this.getOrcaWhirlpools());
}
const whirlpool = whirlpools?.find((x) => x.address === poolString);
if (!pool || !whirlpool) {
throw Error(`Could not get orca pool data for ${poolString}`);
}
const poolInfo = {
dex: 'ORCA',
address: poolPubkey,
tokenMintA: (0, compat_1.fromLegacyPublicKey)(pool.tokenMintA),
tokenMintB: (0, compat_1.fromLegacyPublicKey)(pool.tokenMintB),
price: pool.price,
feeRate: pool.feePercentage,
volumeOnLast7d: whirlpool.volume ? new decimal_js_1.default(whirlpool.volume?.week) : undefined,
tvl: whirlpool.tvl ? new decimal_js_1.default(whirlpool.tvl) : 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, offset: 8n, encoding: 'base58' } },
],
})
.send();
return rawPositions.length;
}
}
exports.OrcaService = OrcaService;
//# sourceMappingURL=OrcaService.js.map
;