@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
747 lines (663 loc) • 25.3 kB
text/typescript
import {
Account,
address,
Address,
Base58EncodedBytes,
getAddressEncoder,
getProgramDerivedAddress,
ProgramDerivedAddress,
Rpc,
SolanaRpcApi,
} from '@solana/kit';
import {
LiquidityDistribution as RaydiumLiquidityDistribuion,
Pool,
RaydiumPoolsResponse,
} from './RaydiumPoolsResponse';
import { fetchMaybePoolState, fetchMaybePersonalPositionState } from '../@codegen/raydium/accounts';
import Decimal from 'decimal.js';
import { WhirlpoolAprApy } from './WhirlpoolAprApy';
import { type WhirlpoolStrategy } from '../@codegen/kliquidity/accounts';
import { unwrapAccount } from '../utils/codamaHelpers';
import {
aprToApy,
GenericPoolInfo,
getStrategyPriceRangeRaydium,
getTokenAccountBalanceLamports,
LiquidityDistribution,
LiquidityForPrice,
optionGetValue,
optionGetValueOrUndefined,
solToWSol,
toLegacyPublicKey,
WSOL_MINT,
ZERO,
} from '../utils';
import axios from 'axios';
import { FullPercentage } from '../utils/CreationParameters';
import { AMM_V3_PROGRAM_ADDRESS } from '../@codegen/raydium/programs';
import { priceToTickIndexWithRounding } from '../utils/raydium';
import {
ApiV3PoolInfoConcentratedItem,
ApiV3PoolInfoCountItem,
ClmmConfigLayout,
ClmmParsedRpcData,
ComputeClmmPoolInfo,
DynamicFeeInfo,
getCollectFeeOnDescription,
PersonalPositionLayout,
POOL_TICK_ARRAY_BITMAP_SEED,
PoolInfoLayout,
PoolUtils,
ReturnTypeFetchExBitmaps,
ReturnTypeFetchMultipleMintInfos,
TickArrayBitmapExtensionLayout,
} from '@raydium-io/raydium-sdk-v2/lib';
import {
RaydiumSqrtPriceMath as SqrtPriceMath,
RaydiumTickMath as TickMath,
ReturnTypeGetTickPrice,
} from '../utils/raydiumSdkCompat';
import { fromLegacyPublicKey } from '@solana/compat';
import { toBN } from '../utils/raydiumBridge';
import { fetchAllMint, Mint } from '@solana-program/token-2022';
import { DEFAULT_PUBLIC_KEY } from '../constants/pubkeys';
import { Logger } from '../utils/Logger';
export class RaydiumService {
private readonly _rpc: Rpc<SolanaRpcApi>;
private readonly _raydiumProgramId: Address;
private readonly _logger: Logger;
constructor(rpc: Rpc<SolanaRpcApi>, raydiumProgramId: Address = AMM_V3_PROGRAM_ADDRESS, logger: Logger = console) {
this._rpc = rpc;
this._raydiumProgramId = raydiumProgramId;
this._logger = logger;
}
getRaydiumProgramId(): Address {
return this._raydiumProgramId;
}
async getRaydiumWhirlpools(): Promise<RaydiumPoolsResponse> {
return (await axios.get<RaydiumPoolsResponse>(`https://api.kamino.finance/v2/raydium/ammPools`)).data;
}
async getRaydiumPoolInfo(
poolPubkey: Address,
poolStats?: RaydiumPoolStatsSource
): Promise<ApiV3PoolInfoConcentratedItem> {
const stats = poolStats ?? (await this.getRaydiumApiPoolInfo(poolPubkey.toString()));
return this.getPoolInfoFromRpc(poolPubkey.toString(), stats);
}
async getRaydiumApiPoolInfo(poolId: string): Promise<ApiV3PoolInfoConcentratedItem | undefined> {
try {
const response = await axios.get<RaydiumApiPoolInfoResponse>(`https://api-v3.raydium.io/pools/info/ids`, {
params: { ids: poolId },
});
const poolList = Array.isArray(response.data) ? response.data : response.data.data;
const poolInfo = poolList?.find((pool) => pool.id === poolId);
if (poolInfo?.type !== 'Concentrated') {
return undefined;
}
return poolInfo;
} catch (error) {
this._logger.warn(`Could not fetch Raydium API pool stats for ${poolId}`, error);
return undefined;
}
}
async getRaydiumPoolLiquidityDistribution(
pool: Address,
keepOrder: boolean = true,
lowestTick?: number,
highestTick?: number
): Promise<LiquidityDistribution> {
const raydiumLiqDistribution = (
await axios.get<RaydiumLiquidityDistribuion>(
`https://api.kamino.finance/v2/raydium/positionLine/${pool.toString()}`
)
).data;
const poolState = unwrapAccount(await fetchMaybePoolState(this._rpc, pool));
if (!poolState) {
throw Error(`Raydium pool state ${pool} does not exist`);
}
const poolPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
toBN(poolState.sqrtPriceX64),
poolState.mintDecimals0,
poolState.mintDecimals1
);
const liqDistribution: LiquidityDistribution = {
currentPrice: poolPrice,
currentTickIndex: poolState.tickCurrent,
distribution: [],
};
raydiumLiqDistribution.data.forEach((entry) => {
const 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 = unwrapAccount(await fetchMaybePersonalPositionState(this._rpc, strategy.position));
if (!position) {
throw Error(`Position ${strategy.position} does not exist`);
}
const poolState = unwrapAccount(await fetchMaybePoolState(this._rpc, 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 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 raydiumPoolInfo = await this.getRaydiumPoolInfo(strategy.pool, raydiumPool);
const params: {
poolInfo: ApiV3PoolInfoConcentratedItem;
aprType: 'day' | 'week' | 'month';
positionTickLowerIndex: number;
positionTickUpperIndex: number;
} = {
poolInfo: raydiumPoolInfo,
aprType: 'day',
positionTickLowerIndex: position.tickLowerIndex,
positionTickUpperIndex: position.tickUpperIndex,
};
const { apr, feeApr, rewardsApr } = PoolUtils.estimateAprsForPriceRangeMultiplier(params);
const totalApr = new Decimal(apr ?? 0).div(100);
const fee = new Decimal(feeApr ?? 0).div(100);
const rewards = (rewardsApr ?? []).map((reward: number) => new Decimal(reward).div(100));
return {
totalApr,
totalApy: aprToApy(totalApr, 365),
feeApr: fee,
feeApy: aprToApy(fee, 365),
rewardsApr: rewards,
rewardsApy: rewards.map((x: Decimal) => aprToApy(x, 365)),
...priceRange,
};
};
getRaydiumPositionAprApy = async (
poolPubkey: Address,
priceLower: Decimal,
priceUpper: Decimal,
pools?: Pool[]
): Promise<WhirlpoolAprApy> => {
const poolState = unwrapAccount(await fetchMaybePoolState(this._rpc, 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 tickLowerIndex = TickMath.getTickWithPriceAndTickspacing(
priceLower,
poolState.tickSpacing,
poolState.mintDecimals0,
poolState.mintDecimals1
);
const 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 poolInfo = await this.getRaydiumPoolInfo(poolPubkey, raydiumPool);
const params: {
poolInfo: ApiV3PoolInfoConcentratedItem;
aprType: 'day' | 'week' | 'month';
positionTickLowerIndex: number;
positionTickUpperIndex: number;
} = {
poolInfo,
aprType: 'day',
positionTickLowerIndex: tickLowerIndex,
positionTickUpperIndex: tickUpperIndex,
};
const { apr, feeApr, rewardsApr } = PoolUtils.estimateAprsForPriceRangeMultiplier(params);
const totalApr = new Decimal(apr ?? 0).div(100);
const fee = new Decimal(feeApr ?? 0).div(100);
const rewards = (rewardsApr ?? []).map((reward: number) => new Decimal(reward).div(100));
return {
totalApr,
totalApy: aprToApy(totalApr, 365),
feeApr: fee,
feeApy: aprToApy(fee, 365),
rewardsApr: rewards,
rewardsApy: rewards.map((x: Decimal) => aprToApy(x, 365)),
...priceRange,
};
};
async getGenericPoolInfo(poolPubkey: Address, pools?: Pool[]): Promise<GenericPoolInfo> {
const poolState = unwrapAccount(await fetchMaybePoolState(this._rpc, 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: GenericPoolInfo = {
dex: 'RAYDIUM',
address: 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: Address): Promise<number> {
const positions = await this._rpc
.getProgramAccounts(this._raydiumProgramId, {
commitment: 'confirmed',
filters: [
{ dataSize: BigInt(PersonalPositionLayout.span) },
{
memcmp: {
bytes: pool.toString() as Base58EncodedBytes,
offset: BigInt(PersonalPositionLayout.offsetOf('poolId')),
encoding: 'base58',
},
},
],
encoding: 'base64+zstd',
})
.send();
return positions.length;
}
getTickPrice(
tick: number,
tokenADecimals: number,
tokenBDecimals: number,
baseIn: boolean = true
): ReturnTypeGetTickPrice {
const tickSqrtPriceX64 = SqrtPriceMath.getSqrtPriceX64FromTick(tick);
const tickPrice = SqrtPriceMath.sqrtPriceX64ToPrice(tickSqrtPriceX64, tokenADecimals, tokenBDecimals);
return baseIn
? { tick, price: tickPrice, tickSqrtPriceX64 }
: { tick, price: new Decimal(1).div(tickPrice), tickSqrtPriceX64 };
}
async getRpcClmmPoolInfo(poolId: string): Promise<ClmmParsedRpcData> {
const poolAccountInfo = await this._rpc
.getAccountInfo(address(poolId), { commitment: 'confirmed', encoding: 'base64' })
.send();
if (!poolAccountInfo.value) {
throw Error(`Raydium pool state ${poolId} does not exist`);
}
const rpc = PoolInfoLayout.decode(Buffer.from(poolAccountInfo.value.data[0], 'base64'));
const currentPrice = SqrtPriceMath.sqrtPriceX64ToPrice(
rpc.sqrtPriceX64,
rpc.mintDecimalsA,
rpc.mintDecimalsB
).toNumber();
const data: ClmmParsedRpcData = {
...rpc,
currentPrice,
programId: toLegacyPublicKey(this._raydiumProgramId),
};
return data;
}
async fetchComputeClmmInfo(poolInfoWithAddress: ClmmRpcDataWithAddress): Promise<ComputeClmmPoolInfo> {
const { address: poolAddress } = poolInfoWithAddress;
return (await this.fetchComputeMultipleClmmInfo([poolInfoWithAddress]))[poolAddress.toString()];
}
async fetchExBitmaps(exBitmapAddress: Address[]): Promise<ReturnTypeFetchExBitmaps> {
const fetchedBitmapAccount = await this._rpc
.getMultipleAccounts(exBitmapAddress, { commitment: 'confirmed', encoding: 'base64' })
.send();
const fetchedBitmapAccountWithAddress = exBitmapAddress.map((address, index) => ({
address,
data: fetchedBitmapAccount.value[index]?.data,
}));
const returnTypeFetchExBitmaps: ReturnTypeFetchExBitmaps = {};
for (const { address, data } of fetchedBitmapAccountWithAddress) {
if (!data) continue;
returnTypeFetchExBitmaps[address.toString()] = TickArrayBitmapExtensionLayout.decode(
Buffer.from(data[0], 'base64')
);
}
return returnTypeFetchExBitmaps;
}
async fetchComputeMultipleClmmInfo(poolList: ClmmRpcDataWithAddress[]): Promise<Record<string, ComputeClmmPoolInfo>> {
const fetchRpcList = poolList.map((p) => p.address);
const rpcRes = await this._rpc
.getMultipleAccounts(fetchRpcList, { commitment: 'confirmed', encoding: 'base64' })
.send();
const rpcResWithAddress = fetchRpcList.map((address, index) => ({
address,
data: rpcRes.value[index]?.data,
}));
const rpcDataMap: Record<string, ReturnType<typeof PoolInfoLayout.decode>> = {};
for (const { address, data } of rpcResWithAddress) {
if (!data) continue;
rpcDataMap[address.toString()] = PoolInfoLayout.decode(Buffer.from(data[0], 'base64'));
}
// Fetch amm configs
const configSet = Array.from(new Set(poolList.map((p) => p.clmmPoolRpcInfo.configId.toBase58())));
const configRes = await this._rpc
.getMultipleAccounts(
configSet.map((s) => address(s)),
{ commitment: 'confirmed', encoding: 'base64' }
)
.send();
const clmmConfigs: Record<string, ReturnType<typeof ClmmConfigLayout.decode>> = {};
configSet.forEach((configAddress, index) => {
const data = configRes.value[index]?.data;
if (data) {
clmmConfigs[configAddress] = ClmmConfigLayout.decode(Buffer.from(data[0], 'base64'));
}
});
// Fetch mint infos
const allMints = Array.from(
new Set(poolList.flatMap((p) => [p.clmmPoolRpcInfo.mintA.toBase58(), p.clmmPoolRpcInfo.mintB.toBase58()]))
);
const mintInfos = await this.fetchMultipleMintInfos(allMints.map((m) => address(m)));
const pdaList = await Promise.all(
poolList.map(async (poolInfo) => {
const pda = await getPdaExBitmapAccount(
fromLegacyPublicKey(poolInfo.clmmPoolRpcInfo.programId),
poolInfo.address
);
return address(pda[0]);
})
);
const exBitData = await this.fetchExBitmaps(pdaList);
const result: Record<string, ComputeClmmPoolInfo> = {};
for (let i = 0; i < poolList.length; i++) {
const cur = poolList[i];
const pda = await getPdaExBitmapAccount(fromLegacyPublicKey(cur.clmmPoolRpcInfo.programId), cur.address);
const ammConfigKey = cur.clmmPoolRpcInfo.configId.toBase58();
const ammConfigData = clmmConfigs[ammConfigKey];
if (!ammConfigData) {
throw new Error(`Raydium AMM config ${ammConfigKey} not found for pool ${cur.address.toString()}`);
}
const mintAKey = cur.clmmPoolRpcInfo.mintA.toBase58();
const mintBKey = cur.clmmPoolRpcInfo.mintB.toBase58();
result[cur.address.toString()] = {
...rpcDataMap[cur.address.toString()],
accInfo: rpcDataMap[cur.address.toString()],
id: toLegacyPublicKey(cur.address),
version: 6,
programId: cur.clmmPoolRpcInfo.programId,
mintA: {
chainId: 101,
address: mintAKey,
programId: mintInfos[mintAKey]?.programId.toBase58() || 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
logoURI: '',
symbol: '',
name: '',
decimals: cur.clmmPoolRpcInfo.mintDecimalsA,
tags: [],
extensions: { feeConfig: mintInfos[mintAKey]?.feeConfig },
},
mintB: {
chainId: 101,
address: mintBKey,
programId: mintInfos[mintBKey]?.programId.toBase58() || 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
logoURI: '',
symbol: '',
name: '',
decimals: cur.clmmPoolRpcInfo.mintDecimalsB,
tags: [],
extensions: { feeConfig: mintInfos[mintBKey]?.feeConfig },
},
ammConfig: {
...ammConfigData,
id: toLegacyPublicKey(address(ammConfigKey)),
fundFeeRate: ammConfigData.fundFeeRate,
description: '',
defaultRange: 0,
defaultRangePoint: [],
fundOwner: ammConfigData.fundOwner.toBase58(),
},
currentPrice: new Decimal(cur.clmmPoolRpcInfo.currentPrice),
exBitmapAccount: toLegacyPublicKey(pda[0]),
exBitmapInfo: exBitData[pda[0].toString()],
startTime: rpcDataMap[cur.address.toString()].startTime.toNumber(),
rewardInfos: rpcDataMap[cur.address.toString()].rewardInfos,
} as any;
}
return result;
}
async fetchMultipleMintInfos(mints: Address[]): Promise<ReturnTypeFetchMultipleMintInfos> {
if (mints.length === 0) return {};
const cleanedMints = mints.map((i) => solToWSol(i));
// fetch in chunks of 100
const mintInfos: Account<Mint>[] = [];
for (let i = 0; i < cleanedMints.length; i += 100) {
const chunk = cleanedMints.slice(i, i + 100);
const chunkMintInfos = await fetchAllMint(this._rpc, chunk);
mintInfos.push(...chunkMintInfos);
}
const mintInfosWithAddress = cleanedMints.map((mint, index) => ({
address: mint,
data: mintInfos[index],
}));
const mintK: ReturnTypeFetchMultipleMintInfos = {};
for (const { address, data } of mintInfosWithAddress) {
if (!data) {
this._logger.error(`Invalid mint ${address}`);
continue;
}
mintK[address] = {
mintAuthority: optionGetValueOrUndefined(data.data.mintAuthority)
? toLegacyPublicKey(optionGetValue(data.data.mintAuthority)!)
: null,
programId: toLegacyPublicKey(data.programAddress),
feeConfig: undefined,
address: toLegacyPublicKey(address),
isInitialized: true,
tlvData: Buffer.from([]), // Raydium SDK requires Buffer
supply: data.data.supply,
decimals: data.data.decimals,
freezeAuthority: optionGetValueOrUndefined(data.data.freezeAuthority)
? toLegacyPublicKey(optionGetValue(data.data.freezeAuthority)!)
: null,
};
}
mintK[DEFAULT_PUBLIC_KEY.toString()] = mintK[WSOL_MINT.toString()];
return mintK;
}
public async getComputeClmmPoolInfo(
clmmPoolRpcInfoWithAddress: ClmmRpcDataWithAddress
): Promise<ComputeClmmPoolInfo> {
const computeClmmPoolInfo = await this.fetchComputeClmmInfo(clmmPoolRpcInfoWithAddress);
return computeClmmPoolInfo;
}
async getPoolInfoFromRpc(poolId: string, poolStats?: RaydiumPoolStatsSource): Promise<ApiV3PoolInfoConcentratedItem> {
const rpcData = await this.getRpcClmmPoolInfo(poolId);
const poolInfoWithAddress = {
address: address(poolId),
clmmPoolRpcInfo: rpcData,
};
const computeClmmPoolInfo = await this.getComputeClmmPoolInfo(poolInfoWithAddress);
const poolInfo = this.clmmComputeInfoToApiInfo(computeClmmPoolInfo, poolStats);
poolInfo.mintAmountA = await getTokenAccountBalanceLamports(this._rpc, fromLegacyPublicKey(rpcData.vaultA));
poolInfo.mintAmountB = await getTokenAccountBalanceLamports(this._rpc, fromLegacyPublicKey(rpcData.vaultB));
return poolInfo;
}
clmmComputeInfoToApiInfo(
pool: ComputeClmmPoolInfo,
poolStats?: RaydiumPoolStatsSource
): ApiV3PoolInfoConcentratedItem {
const hasDynamicFee = DynamicFeeInfo.getDynamicFeeInfo({ poolInfo: pool.accInfo }) !== undefined;
const countStats = normalizeRaydiumPoolStats(poolStats);
return {
...pool,
type: 'Concentrated',
programId: pool.programId.toString(),
id: pool.id.toString(),
rewardDefaultInfos: [],
rewardDefaultPoolInfos: 'Clmm',
price: pool.currentPrice.toNumber(),
mintAmountA: 0,
mintAmountB: 0,
feeRate: pool.ammConfig.tradeFeeRate,
openTime: pool.startTime.toString(),
tvl: Number(poolStats?.tvl ?? 0),
day: countStats.day,
week: countStats.week,
month: countStats.month,
pooltype: [],
farmUpcomingCount: 0,
farmOngoingCount: 0,
farmFinishedCount: 0,
burnPercent: 0,
feeOn: getCollectFeeOnDescription(pool.accInfo.feeOn),
hasDynamicFee,
launchMigratePool: false,
tips: [],
config: {
...pool.ammConfig,
id: pool.ammConfig.id.toString(),
defaultRange: 0,
defaultRangePoint: [],
},
};
}
}
export interface ClmmRpcDataWithAddress {
address: Address;
clmmPoolRpcInfo: ClmmParsedRpcData;
}
type RaydiumApiPoolInfoResponse = { data?: ApiV3PoolInfoConcentratedItem[] } | ApiV3PoolInfoConcentratedItem[];
type RaydiumCountStatsSource = Partial<Omit<ApiV3PoolInfoCountItem, 'rewardApr'>> & {
rewardApr?: unknown;
};
export type RaydiumPoolStatsSource = {
day?: RaydiumCountStatsSource;
week?: RaydiumCountStatsSource;
month?: RaydiumCountStatsSource;
tvl?: number;
};
export async function getPdaExBitmapAccount(programId: Address, poolId: Address): Promise<ProgramDerivedAddress> {
const addressEncoder = getAddressEncoder();
return getProgramDerivedAddress({
seeds: [POOL_TICK_ARRAY_BITMAP_SEED, addressEncoder.encode(poolId)],
programAddress: programId,
});
}
function normalizeRaydiumPoolStats(poolStats?: RaydiumPoolStatsSource): {
day: ApiV3PoolInfoCountItem;
week: ApiV3PoolInfoCountItem;
month: ApiV3PoolInfoCountItem;
} {
return {
day: normalizeRaydiumCountStats(poolStats?.day),
week: normalizeRaydiumCountStats(poolStats?.week),
month: normalizeRaydiumCountStats(poolStats?.month),
};
}
function normalizeRaydiumCountStats(stats?: RaydiumCountStatsSource): ApiV3PoolInfoCountItem {
return {
...emptyRaydiumCountStats,
volume: Number(stats?.volume ?? emptyRaydiumCountStats.volume),
volumeQuote: Number(stats?.volumeQuote ?? emptyRaydiumCountStats.volumeQuote),
volumeFee: Number(stats?.volumeFee ?? emptyRaydiumCountStats.volumeFee),
apr: Number(stats?.apr ?? emptyRaydiumCountStats.apr),
feeApr: Number(stats?.feeApr ?? emptyRaydiumCountStats.feeApr),
priceMin: Number(stats?.priceMin ?? emptyRaydiumCountStats.priceMin),
priceMax: Number(stats?.priceMax ?? emptyRaydiumCountStats.priceMax),
rewardApr: normalizeRewardApr(stats?.rewardApr),
};
}
function normalizeRewardApr(rewardApr: unknown): number[] {
if (Array.isArray(rewardApr)) {
return rewardApr.map((value) => Number(value));
}
if (rewardApr && typeof rewardApr === 'object') {
const keyedRewardApr = rewardApr as Record<string, unknown>;
return ['A', 'B', 'C']
.map((key) => keyedRewardApr[key])
.filter((value) => value !== undefined)
.map((value) => Number(value));
}
return [];
}
const emptyRaydiumCountStats: ApiV3PoolInfoCountItem = {
volume: 0,
volumeQuote: 0,
volumeFee: 0,
apr: 0,
feeApr: 0,
priceMin: 0,
priceMax: 0,
rewardApr: [],
};