UNPKG

@kamino-finance/kliquidity-sdk

Version:

Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol

273 lines (251 loc) 8.02 kB
import { address, Address, Base58EncodedBytes, Rpc, SolanaRpcApi } from '@solana/kit'; import Decimal from 'decimal.js'; import { WhirlpoolStrategy } from '../@codegen/kliquidity/accounts'; import { aprToApy, GenericPoolInfo, getMeteoraPriceLowerUpper, getMintDecimals, getStrategyPriceRangeMeteora, LiquidityDistribution, ZERO, } from '../utils'; import { LbPair, PositionV2 } from '../@codegen/meteora/accounts'; import { WhirlpoolAprApy } from './WhirlpoolAprApy'; import { PROGRAM_ID as METEORA_PROGRAM_ID } from '../@codegen/meteora/programId'; import { getPriceOfBinByBinIdWithDecimals } from '../utils/meteora'; import { DEFAULT_PUBLIC_KEY } from '../constants/pubkeys'; export interface MeteoraPool { key: Address; pool: LbPair; } export class MeteoraService { private readonly _rpc: Rpc<SolanaRpcApi>; private readonly _meteoraProgramId: Address; constructor(rpc: Rpc<SolanaRpcApi>, meteoraProgramId: Address = METEORA_PROGRAM_ID) { this._rpc = rpc; this._meteoraProgramId = meteoraProgramId; } getMeteoraProgramId(): Address { return this._meteoraProgramId; } async getPool(poolAddress: Address): Promise<LbPair | null> { return await LbPair.fetch(this._rpc, poolAddress); } async getPosition(position: Address): Promise<PositionV2 | null> { return await PositionV2.fetch(this._rpc, position); } async getMeteoraPools(): Promise<MeteoraPool[]> { const rawPools = await this._rpc .getProgramAccounts(METEORA_PROGRAM_ID, { commitment: 'confirmed', filters: [{ dataSize: 904n }], }) .send(); const pools: MeteoraPool[] = []; for (let i = 0; i < rawPools.length; i++) { try { const lbPair = LbPair.decode(Buffer.from(rawPools[i].account.data)); pools.push({ pool: lbPair, key: rawPools[i].pubkey }); } catch (e) { console.log(e); } } return pools; } async getStrategyMeteoraPoolAprApy(strategy: WhirlpoolStrategy): Promise<WhirlpoolAprApy> { const position = await this.getPosition(strategy.position); const pool = await this.getPool(strategy.pool); const decimalsX = strategy.tokenAMintDecimals.toNumber(); const decimalsY = strategy.tokenBMintDecimals.toNumber(); let priceLower: Decimal = new Decimal(0); let priceUpper: Decimal = new Decimal(0); if (position && pool) { const priceRange = getMeteoraPriceLowerUpper( position.lowerBinId, position.upperBinId, pool.binStep, decimalsX, decimalsY ); priceLower = priceRange.priceLower; priceUpper = priceRange.priceUpper; } let priceRange = { priceLower, poolPrice: new Decimal(0), priceUpper, strategyOutOfRange: true }; if (pool && position) { priceRange = getStrategyPriceRangeMeteora( priceLower, priceUpper, pool.activeId, pool.binStep, decimalsX, decimalsY ); } if (priceRange.strategyOutOfRange) { return { ...priceRange, rewardsApy: [], rewardsApr: [], feeApy: ZERO, feeApr: ZERO, totalApy: ZERO, totalApr: ZERO, }; } // TODO: fix this const totalApr = new Decimal(0); const feeApr = new Decimal(0); const rewardsApr = [new Decimal(0)]; return { totalApr, totalApy: aprToApy(totalApr, 365), feeApr, feeApy: aprToApy(feeApr, 365), rewardsApr, rewardsApy: rewardsApr.map((x) => aprToApy(x, 365)), ...priceRange, }; } // strongly recommended to pass lowestTick and highestTick because fetching the lowest and highest existent takes very long async getMeteoraLiquidityDistribution( poolKey: Address, keepOrder: boolean = true, lowestTick?: number, highestTick?: number ): Promise<LiquidityDistribution> { // trick the linter (() => { return { keepOrder, highestTick, lowestTick }; })(); //TODO: fix this const pool = await this.getPool(poolKey); if (!pool) { // if the pool doesn't exist, return empty distribution return { currentPrice: new Decimal(0), currentTickIndex: 0, distribution: [], }; } const currentTickIndex = pool.activeId; const tokenXDecimals = await getMintDecimals(this._rpc, pool.tokenXMint); const tokenYDecimals = await getMintDecimals(this._rpc, pool.tokenYMint); const currentPrice = getPriceOfBinByBinIdWithDecimals( currentTickIndex, pool.binStep, tokenXDecimals, tokenYDecimals ); // TODO: add actual distribution return { currentPrice, currentTickIndex, distribution: [], }; } async getMeteoraPositionAprApy( poolPubkey: Address, priceLower: Decimal, priceUpper: Decimal ): Promise<WhirlpoolAprApy> { const pool = await this.getPool(poolPubkey); if (!pool) { return { priceLower: ZERO, priceUpper: ZERO, poolPrice: ZERO, strategyOutOfRange: true, rewardsApy: [], rewardsApr: [], feeApy: ZERO, feeApr: ZERO, totalApy: ZERO, totalApr: ZERO, }; } const tokenXDecimals = await getMintDecimals(this._rpc, pool.tokenXMint); const tokenYDecimals = await getMintDecimals(this._rpc, pool.tokenYMint); const priceRange = getStrategyPriceRangeMeteora( priceLower, priceUpper, pool.activeId, pool.binStep, tokenXDecimals, tokenYDecimals ); if (priceRange.strategyOutOfRange) { return { ...priceRange, rewardsApy: [], rewardsApr: [], feeApy: ZERO, feeApr: ZERO, totalApy: ZERO, totalApr: ZERO, }; } const totalApr = new Decimal(0); const feeApr = new Decimal(0); const rewardsApr = [new Decimal(0)]; return { totalApr, totalApy: aprToApy(totalApr, 365), feeApr, feeApy: aprToApy(feeApr, 365), rewardsApr, rewardsApy: rewardsApr.map((x) => aprToApy(x, 365)), ...priceRange, }; } async getGenericPoolInfo(poolPubkey: Address): Promise<GenericPoolInfo> { const pool = await this.getPool(poolPubkey); if (!pool) { return { dex: 'METEORA', address: DEFAULT_PUBLIC_KEY, tokenMintA: DEFAULT_PUBLIC_KEY, tokenMintB: DEFAULT_PUBLIC_KEY, price: new Decimal(0), feeRate: new Decimal(0), volumeOnLast7d: new Decimal(0), tvl: new Decimal(0), tickSpacing: new Decimal(0), positions: new Decimal(0), }; } const tokenXDecimals = await getMintDecimals(this._rpc, pool.tokenXMint); const tokenYDecimals = await getMintDecimals(this._rpc, pool.tokenYMint); const price = getPriceOfBinByBinIdWithDecimals(pool.activeId, pool.binStep, tokenXDecimals, tokenYDecimals); const poolInfo: GenericPoolInfo = { dex: 'METEORA', address: address(poolPubkey), tokenMintA: pool.tokenXMint, tokenMintB: pool.tokenYMint, price, feeRate: computeMeteoraFee(pool), // TODO: add these volumeOnLast7d: new Decimal(0), tvl: new Decimal(0), tickSpacing: new Decimal(pool.binStep), // todo(Silviu): get real amount of positions positions: new Decimal(await this.getPositionsCountByPool(poolPubkey)), }; return poolInfo; } async getPositionsCountByPool(pool: Address): Promise<number> { const rawPositions = await this._rpc .getProgramAccounts(METEORA_PROGRAM_ID, { commitment: 'confirmed', filters: [ { dataSize: 8120n }, { memcmp: { bytes: pool.toString() as Base58EncodedBytes, offset: 8n, encoding: 'base58' } }, ], }) .send(); return rawPositions.length; } } export function computeMeteoraFee(pool: LbPair): Decimal { return new Decimal(pool.parameters.baseFactor).mul(new Decimal(pool.binStep)).div(new Decimal(1e6)); }