UNPKG

@marinade.finance/kamino-sdk

Version:
321 lines (283 loc) 10.3 kB
import { Connection, PublicKey } from '@solana/web3.js'; import { SolanaCluster } from '@hubbleprotocol/hubble-config'; import { LiquidityDistribution as RaydiumLiquidityDistribuion, Pool, RaydiumPoolsResponse, } from './RaydiumPoolsResponse'; import { PersonalPositionState, PoolState } from '../raydium_client'; import Decimal from 'decimal.js'; import { AmmV3, AmmV3PoolInfo, PositionInfoLayout, TickMath, SqrtPriceMath } from '@raydium-io/raydium-sdk'; import { WhirlpoolAprApy } from './WhirlpoolAprApy'; import { WhirlpoolStrategy } from '../kamino-client/accounts'; import { aprToApy, GenericPoolInfo, getStrategyPriceRangeRaydium, LiquidityDistribution, LiquidityForPrice, ZERO, } from '../utils'; import axios from 'axios'; import { FullPercentage } from '../utils/CreationParameters'; import { PROGRAM_ID as RAYDIUM_PROGRAM_ID } from '../raydium_client/programId'; import { priceToTickIndexWithRounding } from '../utils/raydium'; export class RaydiumService { private readonly _connection: Connection; private readonly _cluster: SolanaCluster; constructor(connection: Connection, cluster: SolanaCluster) { this._connection = connection; this._cluster = cluster; } async getRaydiumWhirlpools(): Promise<RaydiumPoolsResponse> { return (await axios.get<RaydiumPoolsResponse>(`https://api.hubbleprotocol.io/raydium/ammPools`)).data; } async getRaydiumPoolLiquidityDistribution( pool: PublicKey, keepOrder: boolean = true, lowestTick?: number, highestTick?: number ): Promise<LiquidityDistribution> { let raydiumLiqDistribution = ( await axios.get<RaydiumLiquidityDistribuion>( `https://api.hubbleprotocol.io/raydium/positionLine/${pool.toString()}` ) ).data; const poolState = await PoolState.fetch(this._connection, pool); if (!poolState) { throw Error(`Raydium pool state ${pool} does not exist`); } let poolPrice = SqrtPriceMath.sqrtPriceX64ToPrice( poolState.sqrtPriceX64, poolState.mintDecimals0, poolState.mintDecimals1 ); let liqDistribution: LiquidityDistribution = { currentPrice: poolPrice, currentTickIndex: poolState.tickCurrent, distribution: [], }; raydiumLiqDistribution.data.forEach((entry) => { let tickIndex = priceToTickIndexWithRounding(entry.price); if ((lowestTick && tickIndex < lowestTick) || (highestTick && tickIndex > highestTick)) { return; } // if the prevoious entry has the same tick index, add to it if ( liqDistribution.distribution.length > 0 && liqDistribution.distribution[liqDistribution.distribution.length - 1].tickIndex === tickIndex ) { liqDistribution.distribution[liqDistribution.distribution.length - 1].liquidity = liqDistribution.distribution[ liqDistribution.distribution.length - 1 ].liquidity.add(new Decimal(entry.liquidity)); } else { let priceWithOrder = new Decimal(entry.price); if (!keepOrder) { priceWithOrder = new Decimal(1).div(priceWithOrder); } const liq: LiquidityForPrice = { price: new Decimal(priceWithOrder), liquidity: new Decimal(entry.liquidity), tickIndex, }; liqDistribution.distribution.push(liq); } }); return liqDistribution; } getStrategyWhirlpoolPoolAprApy = async (strategy: WhirlpoolStrategy, pools?: Pool[]): Promise<WhirlpoolAprApy> => { const position = await PersonalPositionState.fetch(this._connection, strategy.position); if (!position) { throw Error(`Position ${strategy.position} does not exist`); } const poolState = await PoolState.fetch(this._connection, strategy.pool); if (!poolState) { throw Error(`Raydium pool state ${strategy.pool} does not exist`); } if (!pools) { ({ data: pools } = await this.getRaydiumWhirlpools()); } if (!pools || pools.length === 0) { throw Error(`Could not get Raydium amm pools from Raydium API`); } const raydiumPool = pools.filter((d) => d.id === position.poolId.toString()).shift(); if (!raydiumPool) { throw Error(`Could not get find Raydium amm pool ${strategy.pool} from Raydium API`); } const poolInfo = ( await AmmV3.fetchMultiplePoolInfos({ connection: this._connection, // @ts-ignore poolKeys: [raydiumPool], batchRequest: true, chainTime: new Date().getTime() / 1000, }) )[strategy.pool.toString()].state; const priceRange = getStrategyPriceRangeRaydium( position.tickLowerIndex, position.tickUpperIndex, Number(poolState.tickCurrent.toString()), Number(strategy.tokenAMintDecimals.toString()), Number(strategy.tokenBMintDecimals.toString()) ); if (priceRange.strategyOutOfRange) { return { ...priceRange, rewardsApy: [], rewardsApr: [], feeApy: ZERO, feeApr: ZERO, totalApy: ZERO, totalApr: ZERO, }; } const params: { poolInfo: AmmV3PoolInfo; aprType: 'day' | 'week' | 'month'; positionTickLowerIndex: number; positionTickUpperIndex: number; } = { poolInfo, aprType: 'day', positionTickLowerIndex: position.tickLowerIndex, positionTickUpperIndex: position.tickUpperIndex, }; const { apr, feeApr, rewardsApr } = AmmV3.estimateAprsForPriceRangeMultiplier(params); const totalApr = new Decimal(apr).div(100); const fee = new Decimal(feeApr).div(100); const rewards = rewardsApr.map((reward) => new Decimal(reward).div(100)); return { totalApr, totalApy: aprToApy(totalApr, 365), feeApr: fee, feeApy: aprToApy(fee, 365), rewardsApr: rewards, rewardsApy: rewards.map((x) => aprToApy(x, 365)), ...priceRange, }; }; getRaydiumPositionAprApy = async ( poolPubkey: PublicKey, priceLower: Decimal, priceUpper: Decimal, pools?: Pool[] ): Promise<WhirlpoolAprApy> => { const poolState = await PoolState.fetch(this._connection, poolPubkey); if (!poolState) { throw Error(`Raydium pool state ${poolPubkey} does not exist`); } if (!pools) { ({ data: pools } = await this.getRaydiumWhirlpools()); } if (!pools || pools.length === 0) { throw Error(`Could not get Raydium amm pools from Raydium API`); } const raydiumPool = pools.filter((d) => d.id === poolPubkey.toString()).shift(); if (!raydiumPool) { throw Error(`Could not get find Raydium amm pool ${poolPubkey.toString()} from Raydium API`); } const poolInfo = ( await AmmV3.fetchMultiplePoolInfos({ connection: this._connection, // @ts-ignore poolKeys: [raydiumPool], batchRequest: true, chainTime: new Date().getTime() / 1000, }) )[poolPubkey.toString()].state; let tickLowerIndex = TickMath.getTickWithPriceAndTickspacing( priceLower, poolState.tickSpacing, poolState.mintDecimals0, poolState.mintDecimals1 ); let tickUpperIndex = TickMath.getTickWithPriceAndTickspacing( priceUpper, poolState.tickSpacing, poolState.mintDecimals0, poolState.mintDecimals1 ); const priceRange = getStrategyPriceRangeRaydium( tickLowerIndex, tickUpperIndex, Number(poolState.tickCurrent.toString()), raydiumPool.mintDecimalsA, raydiumPool.mintDecimalsB ); if (priceRange.strategyOutOfRange) { return { ...priceRange, rewardsApy: [], rewardsApr: [], feeApy: ZERO, feeApr: ZERO, totalApy: ZERO, totalApr: ZERO, }; } const params: { poolInfo: AmmV3PoolInfo; aprType: 'day' | 'week' | 'month'; positionTickLowerIndex: number; positionTickUpperIndex: number; } = { poolInfo, aprType: 'day', positionTickLowerIndex: tickLowerIndex, positionTickUpperIndex: tickUpperIndex, }; const { apr, feeApr, rewardsApr } = AmmV3.estimateAprsForPriceRangeMultiplier(params); const totalApr = new Decimal(apr).div(100); const fee = new Decimal(feeApr).div(100); const rewards = rewardsApr.map((reward) => new Decimal(reward).div(100)); return { totalApr, totalApy: aprToApy(totalApr, 365), feeApr: fee, feeApy: aprToApy(fee, 365), rewardsApr: rewards, rewardsApy: rewards.map((x) => aprToApy(x, 365)), ...priceRange, }; }; async getGenericPoolInfo(poolPubkey: PublicKey, pools?: Pool[]) { const poolState = await PoolState.fetch(this._connection, poolPubkey); if (!poolState) { throw Error(`Raydium pool state ${poolPubkey} does not exist`); } if (!pools) { ({ data: pools } = await this.getRaydiumWhirlpools()); } if (!pools || pools.length === 0) { throw Error(`Could not get Raydium amm pools from Raydium API`); } const raydiumPool = pools.filter((d) => d.id === poolPubkey.toString()).shift(); if (!raydiumPool) { throw Error(`Could not get find Raydium amm pool ${poolPubkey.toString()} from Raydium API`); } let poolInfo: GenericPoolInfo = { dex: 'RAYDIUM', address: new PublicKey(poolPubkey), tokenMintA: poolState.tokenMint0, tokenMintB: poolState.tokenMint1, price: new Decimal(raydiumPool.price), feeRate: new Decimal(raydiumPool.ammConfig.tradeFeeRate).div(new Decimal(FullPercentage)), volumeOnLast7d: new Decimal(raydiumPool.week.volume), tvl: new Decimal(raydiumPool.tvl), tickSpacing: new Decimal(raydiumPool.ammConfig.tickSpacing), // todo(Silviu): get real amount of positions positions: new Decimal(0), }; return poolInfo; } async getPositionsCountByPool(pool: PublicKey): Promise<number> { const positions = await this._connection.getProgramAccounts(RAYDIUM_PROGRAM_ID, { commitment: 'confirmed', filters: [ { dataSize: PositionInfoLayout.span }, { memcmp: { bytes: pool.toBase58(), offset: PositionInfoLayout.offsetOf('poolId') } }, ], }); return positions.length; } }