UNPKG

@kamino-finance/kliquidity-sdk

Version:

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

278 lines 12.1 kB
"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