@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
278 lines • 12.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MeteoraService = void 0;
exports.computeMeteoraFee = computeMeteoraFee;
const kit_1 = require("@solana/kit");
const bytes_1 = require("../utils/bytes");
const decimal_js_1 = __importDefault(require("decimal.js"));
const axios_1 = __importDefault(require("axios"));
const utils_1 = require("../utils");
const accounts_1 = require("../@codegen/meteora/accounts");
const accounts_2 = require("../@codegen/meteora/accounts");
const codamaHelpers_1 = require("../utils/codamaHelpers");
const programs_1 = require("../@codegen/meteora/programs");
const meteora_1 = require("../utils/meteora");
const pubkeys_1 = require("../constants/pubkeys");
const fzstd_1 = require("fzstd");
class MeteoraService {
_rpc;
_meteoraProgramId;
_meteoraApiUrl;
_logger;
constructor(rpc, meteoraProgramId = programs_1.LB_CLMM_PROGRAM_ADDRESS, logger = console) {
this._rpc = rpc;
this._meteoraProgramId = meteoraProgramId;
this._meteoraApiUrl = 'https://dlmm-api.meteora.ag';
this._logger = logger;
}
getMeteoraProgramId() {
return this._meteoraProgramId;
}
async getPool(poolAddress) {
return (0, codamaHelpers_1.unwrapAccount)(await (0, accounts_1.fetchMaybeLbPair)(this._rpc, poolAddress));
}
async getPosition(position) {
return (0, codamaHelpers_1.unwrapAccount)(await (0, accounts_2.fetchMaybePositionV2)(this._rpc, position));
}
async getMeteoraPools() {
const rawPools = await this._rpc
.getProgramAccounts(programs_1.LB_CLMM_PROGRAM_ADDRESS, {
commitment: 'confirmed',
filters: [{ dataSize: 904n }],
encoding: 'base64+zstd',
})
.send();
const pools = [];
for (let i = 0; i < rawPools.length; i++) {
try {
const compressedData = (0, bytes_1.base64ToBytes)(rawPools[i].account.data[0]);
const decompressedData = (0, fzstd_1.decompress)(compressedData);
const lbPair = (0, accounts_1.getLbPairDecoder)().decode(new Uint8Array(decompressedData));
pools.push({ pool: lbPair, key: rawPools[i].pubkey });
}
catch (e) {
this._logger.error('Error getting Meteora pools from chain:', e);
}
}
return pools;
}
// Fetch all Meteora pools from their API with pagination support
async getMeteoraPoolsFromAPI(tokens = [], maxPages = 300, maxPageSize = 100) {
const allPoolsAPI = [];
let offset = 0;
let hasMore = true;
let pageCount = 0;
// First, fetch all pool data from the API
while (hasMore && pageCount < maxPages) {
pageCount++;
const url = new URL(`${this._meteoraApiUrl}/pair/all_with_pagination`);
url.searchParams.set('limit', maxPageSize.toString());
url.searchParams.set('offset', offset.toString());
// Add token filtering if provided
if (tokens.length === 1) {
url.searchParams.set('token', tokens[0]);
}
else if (tokens.length === 2) {
url.searchParams.set('token_x', tokens[0]);
url.searchParams.set('token_y', tokens[1]);
}
const response = await axios_1.default.get(url.toString());
const data = response.data;
// Add pools from this page to our collection
// The Meteora API returns { pairs: [...pools...], total: number }
if (data.pairs && Array.isArray(data.pairs) && data.pairs.length > 0) {
allPoolsAPI.push(...data.pairs);
offset += data.pairs.length;
}
// Check if there are more pages
const responseLength = data.pairs ? data.pairs.length : 0;
if (responseLength < maxPageSize) {
hasMore = false;
}
// Check total if available
if (data.total && allPoolsAPI.length >= data.total) {
hasMore = false;
}
// Check pagination metadata if available
if (data.meta) {
if (data.meta.cursor && !data.meta.cursor.next) {
hasMore = false;
}
if (data.meta.total && allPoolsAPI.length >= data.meta.total) {
hasMore = false;
}
}
}
if (pageCount >= maxPages) {
throw new Error(`Reached maximum page limit (${maxPages}). There might be more pools available.`);
}
return allPoolsAPI;
}
async getStrategyMeteoraPoolAprApy(strategy) {
const position = await this.getPosition(strategy.position);
const pool = await this.getPool(strategy.pool);
const decimalsX = Number(strategy.tokenAMintDecimals);
const decimalsY = Number(strategy.tokenBMintDecimals);
let priceLower = new decimal_js_1.default(0);
let priceUpper = new decimal_js_1.default(0);
if (position && pool) {
const priceRange = (0, utils_1.getMeteoraPriceLowerUpper)(position.lowerBinId, position.upperBinId, pool.binStep, decimalsX, decimalsY);
priceLower = priceRange.priceLower;
priceUpper = priceRange.priceUpper;
}
let priceRange = { priceLower, poolPrice: new decimal_js_1.default(0), priceUpper, strategyOutOfRange: true };
if (pool && position) {
priceRange = (0, utils_1.getStrategyPriceRangeMeteora)(priceLower, priceUpper, pool.activeId, pool.binStep, decimalsX, decimalsY);
}
if (priceRange.strategyOutOfRange) {
return {
...priceRange,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
// TODO: fix this
const totalApr = new decimal_js_1.default(0);
const feeApr = new decimal_js_1.default(0);
const rewardsApr = [new decimal_js_1.default(0)];
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 getMeteoraLiquidityDistribution(poolKey, keepOrder = true, lowestTick, highestTick) {
// 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_js_1.default(0),
currentTickIndex: 0,
distribution: [],
};
}
const currentTickIndex = pool.activeId;
const tokenXDecimals = await (0, utils_1.getMintDecimals)(this._rpc, pool.tokenXMint);
const tokenYDecimals = await (0, utils_1.getMintDecimals)(this._rpc, pool.tokenYMint);
const currentPrice = (0, meteora_1.getPriceOfBinByBinIdWithDecimals)(currentTickIndex, pool.binStep, tokenXDecimals, tokenYDecimals);
// TODO: add actual distribution
return {
currentPrice,
currentTickIndex,
distribution: [],
};
}
async getMeteoraPositionAprApy(poolPubkey, priceLower, priceUpper) {
const pool = await this.getPool(poolPubkey);
if (!pool) {
return {
priceLower: utils_1.ZERO,
priceUpper: utils_1.ZERO,
poolPrice: utils_1.ZERO,
strategyOutOfRange: true,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const tokenXDecimals = await (0, utils_1.getMintDecimals)(this._rpc, pool.tokenXMint);
const tokenYDecimals = await (0, utils_1.getMintDecimals)(this._rpc, pool.tokenYMint);
const priceRange = (0, utils_1.getStrategyPriceRangeMeteora)(priceLower, priceUpper, pool.activeId, pool.binStep, tokenXDecimals, tokenYDecimals);
if (priceRange.strategyOutOfRange) {
return {
...priceRange,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const totalApr = new decimal_js_1.default(0);
const feeApr = new decimal_js_1.default(0);
const rewardsApr = [new decimal_js_1.default(0)];
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,
};
}
async getGenericPoolInfo(poolPubkey) {
const pool = await this.getPool(poolPubkey);
if (!pool) {
return {
dex: 'METEORA',
address: pubkeys_1.DEFAULT_PUBLIC_KEY,
tokenMintA: pubkeys_1.DEFAULT_PUBLIC_KEY,
tokenMintB: pubkeys_1.DEFAULT_PUBLIC_KEY,
price: new decimal_js_1.default(0),
feeRate: new decimal_js_1.default(0),
volumeOnLast7d: new decimal_js_1.default(0),
tvl: new decimal_js_1.default(0),
tickSpacing: new decimal_js_1.default(0),
positions: new decimal_js_1.default(0),
};
}
const tokenXDecimals = await (0, utils_1.getMintDecimals)(this._rpc, pool.tokenXMint);
const tokenYDecimals = await (0, utils_1.getMintDecimals)(this._rpc, pool.tokenYMint);
const price = (0, meteora_1.getPriceOfBinByBinIdWithDecimals)(pool.activeId, pool.binStep, tokenXDecimals, tokenYDecimals);
const poolInfo = {
dex: 'METEORA',
address: (0, kit_1.address)(poolPubkey),
tokenMintA: pool.tokenXMint,
tokenMintB: pool.tokenYMint,
price,
feeRate: computeMeteoraFee(pool),
// TODO: add these
volumeOnLast7d: new decimal_js_1.default(0),
tvl: new decimal_js_1.default(0),
tickSpacing: new decimal_js_1.default(pool.binStep),
// todo(Silviu): get real amount of positions
positions: new decimal_js_1.default(await this.getPositionsCountByPool(poolPubkey)),
};
return poolInfo;
}
async getPositionsCountByPool(pool) {
const rawPositions = await this._rpc
.getProgramAccounts(programs_1.LB_CLMM_PROGRAM_ADDRESS, {
commitment: 'confirmed',
filters: [
{ dataSize: 8120n },
{ memcmp: { bytes: pool.toString(), offset: 8n, encoding: 'base58' } },
],
encoding: 'base64+zstd',
})
.send();
return rawPositions.length;
}
}
exports.MeteoraService = MeteoraService;
function computeMeteoraFee(pool) {
return new decimal_js_1.default(pool.parameters.baseFactor).mul(new decimal_js_1.default(pool.binStep)).div(new decimal_js_1.default(1e6));
}
//# sourceMappingURL=MeteoraService.js.map