orca-clmm-agent
Version:
Orca Whirlpool clmm library for automated position management
539 lines (538 loc) • 26.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLiquidityInTicks = exports.divergenceLoss = exports.openPosition = exports.getOnChainPool = exports.closePositionAndHarvestYield = exports.getEstimatedYield = exports.getOrcaPositions = exports.fetchOrcaPoolByAddress = exports.getPreloadedOrcaTokens = exports.preloadOrcaTokens = exports.getOrcaTokens = exports.fetchOrcaPools = exports.convertPositionFees = exports.getTickArrayStartIndex = exports.calculateMaxTickIndex = exports.calculateMinTickIndex = exports.setOrcaDefaultFunder = exports.INVALID_START_TICK_ERROR = exports.TOKEN_MIN_SUBCEEDED_ERROR = exports.TOKEN_MAX_EXCEEDED_ERROR = void 0;
const whirlpools_1 = require("@orca-so/whirlpools");
const whirlpools_core_1 = require("@orca-so/whirlpools-core");
const types_1 = require("./types");
const solana_1 = require("./solana");
const utils_1 = require("./utils");
const lifi_1 = require("./lifi");
const whirlpools_client_1 = require("@orca-so/whirlpools-client");
const date_fns_1 = require("date-fns");
const kit_1 = require("@solana/kit");
const orca_types_1 = require("./orca.types");
// Constants for tick array configuration
const TICK_ARRAY_SIZE = 88; // Number of physical ticks in a tick array
// The absolute min/max tick indices for Orca Whirlpools
// For tickspacing=1, these are -443636 and 443636
const ABSOLUTE_MIN_TICK_INDEX = -443636;
const ABSOLUTE_MAX_TICK_INDEX = 443636;
const PRELOADED_TOKENS = { tokens: [], time: new Date() };
//https://github.com/orca-so/whirlpools/blob/main/programs/whirlpool/src/errors.rs
exports.TOKEN_MAX_EXCEEDED_ERROR = BigInt(6017);
exports.TOKEN_MIN_SUBCEEDED_ERROR = BigInt(6018);
exports.INVALID_START_TICK_ERROR = BigInt(6001);
exports.setOrcaDefaultFunder = whirlpools_1.setDefaultFunder;
/**
* Calculates the minimum tick index based on the pool's tickspacing
* @param tickSpacing The tickspacing of the pool
* @returns The minimum tick index that is a valid multiple of tickSpacing
*/
const calculateMinTickIndex = (tickSpacing) => {
// The min tick index should be a multiple of tickSpacing
return Math.ceil(ABSOLUTE_MIN_TICK_INDEX / tickSpacing) * tickSpacing;
};
exports.calculateMinTickIndex = calculateMinTickIndex;
/**
* Calculates the maximum tick index based on the pool's tickspacing
* @param tickSpacing The tickspacing of the pool
* @returns The maximum tick index that is a valid multiple of tickSpacing
*/
const calculateMaxTickIndex = (tickSpacing) => {
// The max tick index should be a multiple of tickSpacing
return Math.floor(ABSOLUTE_MAX_TICK_INDEX / tickSpacing) * tickSpacing;
};
exports.calculateMaxTickIndex = calculateMaxTickIndex;
/**
* Calculates the start tick index for a tick array given a tick index and tickspacing
* @param tickIndex The current tick index
* @param tickSpacing The tick spacing of the pool
* @returns The start tick index of the tick array containing the given tick
*/
const getTickArrayStartIndex = (tickIndex, tickSpacing) => {
const ticksInArray = TICK_ARRAY_SIZE * tickSpacing;
// Find the closest multiple of (tickSpacing * TICK_ARRAY_SIZE) that is less than or equal to tickIndex
return Math.floor(tickIndex / ticksInArray) * ticksInArray;
};
exports.getTickArrayStartIndex = getTickArrayStartIndex;
/**
* Converts tick index to price, handling edge cases for infinite ranges
* @param tickIndex The tick index to convert
* @param decimalsA Token A decimals
* @param decimalsB Token B decimals
* @param tickSpacing The tick spacing of the pool
* @returns The price or undefined for infinite bounds
*/
const convertTickToPrice = (tickIndex, decimalsA, decimalsB, tickSpacing) => {
// Check if the tick index is at the min/max boundaries for this tickSpacing
const minTick = (0, exports.calculateMinTickIndex)(tickSpacing);
const maxTick = (0, exports.calculateMaxTickIndex)(tickSpacing);
if (tickIndex === minTick)
return 0;
if (tickIndex === maxTick)
return Infinity;
// For normal ticks, convert to price
return (0, whirlpools_core_1.tickIndexToPrice)(tickIndex, decimalsA, decimalsB);
};
/**
* Converts position fees to decimal format
* @param feeQuote The fee quote object containing feeOwedA and feeOwedB
* @param tokenADecimals Decimals for token A
* @param tokenBDecimals Decimals for token B
* @returns Object with converted fee amounts
*/
const convertPositionFees = (feeQuote, tokenADecimals, tokenBDecimals) => {
return {
feeAmountA: (0, utils_1.convertRawToDecimal)(feeQuote.feeOwedA, tokenADecimals),
feeAmountB: (0, utils_1.convertRawToDecimal)(feeQuote.feeOwedB, tokenBDecimals),
};
};
exports.convertPositionFees = convertPositionFees;
/**
* Fetches all Orca Whirlpool pools from the API
* @returns A promise that resolves to an array of WhirlpoolResponse objects
*/
const fetchOrcaPools = async () => {
let continueFetching = true;
const whirlpools = [];
let cursor = null;
while (continueFetching) {
const url = `https://api.orca.so/v2/solana/pools?size=1000${cursor ? `&after=${cursor}` : ""}&stats=5m,15m,30m,1H,2H,4H,8H,24H,7D,30D`;
const whirlpoolsResponse = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
// no cache
"Cache-Control": "no-cache, no-store, must-revalidate",
},
});
const status = whirlpoolsResponse.status;
if (status !== 200) {
throw new Error(`Failed to fetch pools: ${status}`);
}
const whirlpoolsData = (await whirlpoolsResponse.json());
whirlpools.push(...whirlpoolsData.data);
if (!whirlpoolsData.meta.cursor) {
console.log("[fetchOrcaPools] No cursor found", url);
continueFetching = false;
break;
}
if (!whirlpoolsData.meta.cursor.next) {
continueFetching = false;
break;
}
cursor = whirlpoolsData.meta.cursor.next;
}
return whirlpools;
};
exports.fetchOrcaPools = fetchOrcaPools;
const getOrcaTokens = async () => {
let continueFetching = true;
const tokens = [];
let cursor = null;
while (continueFetching) {
const tokensResponse = await fetch(`https://api.orca.so/v2/solana/tokens?size=1000${cursor ? `&after=${cursor}` : ""}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
// no cache
"Cache-Control": "no-cache, no-store, must-revalidate",
},
});
const status = tokensResponse.status;
if (status !== 200) {
throw new Error(`Failed to fetch tokens: ${status}`);
}
const tokensData = (await tokensResponse.json());
if (!tokensData.meta.cursor.next) {
continueFetching = false;
}
tokens.push(...tokensData.data);
cursor = tokensData.meta.cursor.next;
}
return tokens;
};
exports.getOrcaTokens = getOrcaTokens;
const preloadOrcaTokens = async () => {
const tokens = await (0, exports.getOrcaTokens)();
PRELOADED_TOKENS.tokens = tokens;
PRELOADED_TOKENS.time = new Date();
return PRELOADED_TOKENS.tokens;
};
exports.preloadOrcaTokens = preloadOrcaTokens;
const getPreloadedOrcaTokens = async (maxAgeInSeconds = 300) => {
if (PRELOADED_TOKENS.tokens.length === 0 || (0, date_fns_1.differenceInSeconds)(new Date(), PRELOADED_TOKENS.time) > maxAgeInSeconds) {
await (0, exports.preloadOrcaTokens)();
}
return PRELOADED_TOKENS.tokens;
};
exports.getPreloadedOrcaTokens = getPreloadedOrcaTokens;
/**
* Fetches a specific Orca Whirlpool pool by address from the API
* @param address The address of the Whirlpool pool
* @returns A promise that resolves to a WhirlpoolResponse object
*/
const fetchOrcaPoolByAddress = async (address) => {
const whirlpoolsResponse = await fetch(`https://api.orca.so/v2/solana/pools/${address}?stats=5m,15m,30m,1H,2H,4H,8H,24H,7D,30D`, {
method: "GET",
headers: {
"Content-Type": "application/json",
// no cache
"Cache-Control": "no-cache, no-store, must-revalidate",
},
});
const whirlpoolsData = (await whirlpoolsResponse.json());
return whirlpoolsData.data;
};
exports.fetchOrcaPoolByAddress = fetchOrcaPoolByAddress;
/**
* Fetches and analyzes Orca Whirlpool positions for a given wallet address
* @param walletAddress - The wallet address to fetch positions for
* @param rpc - The RPC connection to use
* @param pools - Optional list of whirlpools to fetch positions for
* @returns A promise that resolves when all positions have been processed
*/
const getOrcaPositions = async (walletAddress, rpc, pools, funder) => {
try {
// Fetch whirlpool list from Orca API
const whirlpools = pools || (await (0, exports.fetchOrcaPools)());
//console.log(`Fetched ${whirlpools.length} whirlpools`);
const whirlpoolsMap = new Map(whirlpools.map((pool) => [pool.address, pool]));
const owner = (0, kit_1.address)(walletAddress);
// Fetch all positions for the owner
const positions = await (0, whirlpools_1.fetchPositionsForOwner)(rpc, owner);
const mappedPositions = [];
for (const position of positions) {
if (!(0, types_1.isPositionBundle)(position)) {
const whirlpoolAddr = position.data.whirlpool.toString();
// Get whirlpool info from our map
let whirlpoolInfo = whirlpoolsMap.get(whirlpoolAddr);
if (!whirlpoolInfo) {
whirlpoolInfo = await (0, exports.fetchOrcaPoolByAddress)(whirlpoolAddr);
if (!whirlpoolInfo) {
console.log(`[getOrcaPositions] No whirlpool info found for ${whirlpoolAddr}`);
continue;
}
whirlpoolsMap.set(whirlpoolAddr, whirlpoolInfo);
}
const onChainPool = await (0, exports.getOnChainPool)(whirlpoolInfo, rpc);
const price = onChainPool.price;
// Get the tickspacing from the whirlpool info
const tickSpacing = whirlpoolInfo.tickSpacing;
const sqrtPrice = (0, whirlpools_core_1.priceToSqrtPrice)(price, whirlpoolInfo.tokenA.decimals, whirlpoolInfo.tokenB.decimals);
const isInRange = (0, whirlpools_core_1.isPositionInRange)(sqrtPrice, position.data.tickLowerIndex, position.data.tickUpperIndex);
const lowerPrice = convertTickToPrice(position.data.tickLowerIndex, whirlpoolInfo.tokenA.decimals, whirlpoolInfo.tokenB.decimals, tickSpacing);
const upperPrice = convertTickToPrice(position.data.tickUpperIndex, whirlpoolInfo.tokenA.decimals, whirlpoolInfo.tokenB.decimals, tickSpacing);
const { positionMint } = position.data;
const slippageToleranceBps = 100;
const { feesQuote, quote: closeQuote } = await (0, whirlpools_1.closePositionInstructions)(rpc, positionMint, slippageToleranceBps, funder);
const fees = (0, exports.convertPositionFees)(feesQuote, whirlpoolInfo.tokenA.decimals, whirlpoolInfo.tokenB.decimals);
const obj = {
//TODO: check: sometimes token symbols seem to be other way around
name: `${whirlpoolInfo.tokenA.symbol}/${whirlpoolInfo.tokenB.symbol}`,
fees,
isInRange,
whirlpool: {
address: whirlpoolInfo.address,
price: whirlpoolInfo.price,
tickSpacing: whirlpoolInfo.tickSpacing,
},
address: position.address,
data: {
liquidity: position.data.liquidity.toString(),
positionMint: position.data.positionMint,
tickLowerIndex: position.data.tickLowerIndex,
tickUpperIndex: position.data.tickUpperIndex,
feeGrowthCheckpointA: position.data.feeGrowthCheckpointA.toString(),
feeGrowthCheckpointB: position.data.feeGrowthCheckpointB.toString(),
},
closeQuote: {
tokenEstA: closeQuote.tokenEstA.toString(),
tokenMinA: closeQuote.tokenMinA.toString(),
tokenEstB: closeQuote.tokenEstB.toString(),
tokenMinB: closeQuote.tokenMinB.toString(),
},
tokenA: whirlpoolInfo.tokenA,
tokenB: whirlpoolInfo.tokenB,
currentMarketPrice: price.toString(),
lowerPrice,
upperPrice,
};
mappedPositions.push(obj);
}
}
return mappedPositions;
}
catch (error) {
console.error("Error fetching Orca positions:", error);
throw error;
}
};
exports.getOrcaPositions = getOrcaPositions;
/**
* Calculates the estimated yield for a given position.
* @param params - The parameters for the yield calculation.
* @returns The estimated yield.
* @throws Will throw an error if the position is not found or if the stats are not available.
*/
const getEstimatedYield = async (params) => {
const { poolAddress, tokenAAmountUSD, tokenBAmountUSD, statsType } = params;
let { liquidity, fees, rewards } = params;
let { pool } = params;
if (!tokenAAmountUSD && !tokenBAmountUSD)
throw new Error("At least one of tokenAAmountUSD or tokenBAmountUSD must be provided");
if (tokenAAmountUSD && tokenBAmountUSD)
throw new Error("Only one of tokenAAmountUSD or tokenBAmountUSD should be provided");
if (!pool)
pool = await (0, exports.fetchOrcaPoolByAddress)(poolAddress);
if (poolAddress !== pool.address)
throw new Error("Pool address does not match");
const price = Number(pool.price);
if (!liquidity)
liquidity = Number(pool.liquidity);
const priceResults = await Promise.all([(0, utils_1.getUSDPrice)({ mintAddress: pool.tokenA.address }), (0, utils_1.getUSDPrice)({ mintAddress: pool.tokenB.address })]);
const tokenAPrice = priceResults[0];
const tokenBPrice = priceResults[1];
const tokenAAmount = tokenAAmountUSD ? tokenAAmountUSD / tokenAPrice : undefined;
const tokenBAmount = tokenBAmountUSD ? tokenBAmountUSD / tokenBPrice : undefined;
let lowerMultiple = 0;
let upperMultiple = 0;
if ("range" in params) {
const { range } = params;
lowerMultiple = 1 - range / 2;
upperMultiple = 1 + range / 2;
}
else if ("lowerLimit" in params && "upperLimit" in params) {
const { lowerLimit, upperLimit } = params;
lowerMultiple = lowerLimit / price;
upperMultiple = upperLimit / price;
}
else
throw new Error("Range or lowerLimit and upperLimit must be provided");
const increaseLiquidityQuote = tokenAAmount ? whirlpools_core_1.increaseLiquidityQuoteB : whirlpools_core_1.increaseLiquidityQuoteA;
let lamports = 0n;
if (tokenAAmount)
lamports = (0, utils_1.convertDecimalToRaw)(tokenAAmount, pool.tokenA.decimals);
if (tokenBAmount)
lamports = (0, utils_1.convertDecimalToRaw)(tokenBAmount, pool.tokenB.decimals);
// const MAX_U64 = (2n ** 64n) - 1n;
// if (lamports > MAX_U64) throw new Error('Requested amount too large for u64');
const lowerTick = (0, whirlpools_core_1.getInitializableTickIndex)((0, whirlpools_core_1.priceToTickIndex)(price * lowerMultiple, pool.tokenA.decimals, pool.tokenB.decimals), pool.tickSpacing);
const upperTick = (0, whirlpools_core_1.getInitializableTickIndex)((0, whirlpools_core_1.priceToTickIndex)(price * upperMultiple, pool.tokenA.decimals, pool.tokenB.decimals), pool.tickSpacing);
const quote = increaseLiquidityQuote(lamports, 100, //slippageToleranceBps
(0, whirlpools_core_1.priceToSqrtPrice)(price, pool.tokenA.decimals, pool.tokenB.decimals), lowerTick, upperTick);
//TODO: calculate with avg pool liq from last x h / days for more accurate yield
const liqShare = Number(quote.liquidityDelta) / liquidity;
const stats = pool.stats[statsType || "24h"];
if (!stats)
throw new Error(`No stats found for ${statsType}`);
const tokenAValue = (0, utils_1.convertRawToDecimal)(BigInt(quote.tokenEstA), pool.tokenA.decimals) * tokenAPrice;
const tokenBValue = (0, utils_1.convertRawToDecimal)(BigInt(quote.tokenEstB), pool.tokenB.decimals) * tokenBPrice;
const positionValue = tokenAValue + tokenBValue;
//expcted 24h yield
if (!fees)
fees = Number(stats.fees);
if (!rewards)
rewards = Number(stats.rewards);
const expYield = ((fees + rewards) * liqShare) / positionValue;
return expYield;
};
exports.getEstimatedYield = getEstimatedYield;
/**
* Closes a position and harvests yield
* @param rpc The RPC client
* @param wallet The wallet signer
* @param position The position to close
* @returns A promise that resolves when the position is closed
* @throws Will throw an error if the position is not found
* @throws SolanaError if the transaction fails
*/
const closePositionAndHarvestYield = async (rpc, wallet, position) => {
const positionMint = (0, kit_1.address)(position.data.positionMint);
const { instructions: closeInstructions, feesQuote, rewardsQuote } = await (0, whirlpools_1.closePositionInstructions)(rpc, positionMint);
console.log("[closePositionAndHarvestYield] got close instructions");
// const rewardsOwed = rewardsQuote.rewards.reduce((acc, reward) => acc + Number(reward.rewardsOwed), 0);
// const hasOwedFees = feesQuote.feeOwedA > 0 || feesQuote.feeOwedB > 0;
// const hasOwedRewards = rewardsOwed > 0;
// const instructions: Instruction[] = [];
// let harvestDetails: TransactionDetails | undefined;
// //if outstanding fees or rewards - harvest
// if (hasOwedFees || hasOwedRewards) {
// const { instructions: harvestInstructions } = await harvestPositionInstructions(rpc, positionMint);
// if (joinInstructions) instructions.push(...harvestInstructions);
// else {
// const { signature, details } = await executeInstructions(rpc, wallet, harvestInstructions);
// harvestDetails = details;
// console.log("[closePositionAndHarvestYield] executed harvest instructions");
// await sleep(500);
// }
// }
// instructions.push(...closeInstructions);
console.log("[closePositionAndHarvestYield] executing final instructions");
const { signature, details } = await (0, solana_1.executeInstructions)(rpc, wallet, closeInstructions);
if (!signature)
throw new Error("Failed to close position");
console.log(`Position closed: ${signature}`);
// if (harvestDetails) {
// details.changes = joinTransactionChanges(harvestDetails.changes, details.changes);
// details.feeUSD = details.feeUSD + harvestDetails.feeUSD;
// }
return {
details,
signature,
};
};
exports.closePositionAndHarvestYield = closePositionAndHarvestYield;
const getOnChainPool = async (whirlpool, rpc) => {
const result = await (0, whirlpools_client_1.fetchWhirlpool)(rpc, (0, kit_1.address)(whirlpool.address));
const { data: pool } = result;
const price = (0, whirlpools_core_1.sqrtPriceToPrice)(pool.sqrtPrice, whirlpool.tokenA.decimals, whirlpool.tokenB.decimals);
return {
...pool,
price,
};
};
exports.getOnChainPool = getOnChainPool;
const SKIP_DUST_THRESHOLD = Number(process.env.SKIP_DUST_THRESHOLD || 0.1);
const openPosition = async ({ rpc, whirlpoolAddress, params, price, lowerMultiple, upperMultiple, slippageToleranceBps, wallet, swapDustToAddress, walletByteArray, maxGasUSD, }) => {
const { instructions, positionMint, quote, initializationCost } = await (0, whirlpools_1.openPositionInstructions)(rpc, whirlpoolAddress, params, price * lowerMultiple, price * upperMultiple, slippageToleranceBps, wallet);
const initCostDecimal = (0, utils_1.convertRawToDecimal)(initializationCost, 9); //SOL
//non-refundable cost
if (initCostDecimal > 0)
throw new orca_types_1.OrcaError(`[openPosition] Initialization cost too high: ${initCostDecimal} SOL`, 999n);
const { signature, details } = await (0, solana_1.executeInstructions)(rpc, wallet, instructions);
let swapSignature;
let swapLoss = 0;
if (swapDustToAddress) {
if (!walletByteArray)
throw new Error("Private key needed to swap dust");
console.log(`Swapping dust`);
const walletChanges = details.changes.filter((c) => c.owner === wallet.address);
const tokenChanges = walletChanges.filter((c) => c.mint !== swapDustToAddress && c.mint !== solana_1.SOL_MINT_ADDRESS);
for (const change of tokenChanges) {
if (change.mint === positionMint)
continue;
const fromAmount = change.amount.toString();
//TODO: if dust is about 2 USD cents, skip swap as gas fee is same or even higher - not worth it
const price = await (0, utils_1.getUSDPrice)({ mintAddress: change.mint });
const amountUSD = change.amountDecimal * price;
// dust is valued less than 10 cents
if (amountUSD < SKIP_DUST_THRESHOLD) {
console.log(`Skipping swap for dust ${change.mint} ${amountUSD}`);
continue;
}
let retries = 0;
while (retries < 3) {
const maxPriceImpact = retries > 0 ? 0.05 : 0.02; //default
try {
const swapDetails = await (0, lifi_1.swapAssets)({
rpc,
fromAmount,
fromTokenAddress: change.mint,
toTokenAddress: swapDustToAddress,
walletByteArray,
maxPriceImpact,
maxGasUSD,
maxRetries: 5, // default
});
swapSignature = swapDetails.signature;
details.feeUSD += swapDetails.feeUSD;
swapLoss += swapDetails.valueLoss;
// TODO: join changes on mint address and sum up changes
// details.changes.push(...swapDetails.details.changes);
console.log(`Successfully swapped ${fromAmount} ${change.mint} to ${swapDustToAddress}`);
break;
}
catch (error) {
await (0, utils_1.sleep)(1000 * (retries + 1));
console.error(`Failed to swap ${fromAmount} ${change.mint} to ${swapDustToAddress}`, error?.message || error);
}
retries++;
}
}
}
return {
positionMint,
signature,
details,
swapSignature,
swapLoss,
};
};
exports.openPosition = openPosition;
/**
* Calculate divergence (impermanent) loss and details in a CLMM range.
* Basically just the loss in value difference form just HODLing the tokens vs LPing them.
* @param p Current price (tokenB per tokenA)
* @param p_i Initial price at position entry
* @param p_a Lower price bound of the range
* @param p_b Upper price bound of the range
* @param depositA Initial deposit amount of token A
* @param depositB Initial deposit amount of token B
* @returns Object with impermanent loss and related values
*/
const divergenceLoss = (p, p_i, p_a, p_b, depositA, depositB) => {
if (p_a >= p_b)
throw new Error("Invalid range");
const sqrt = Math.sqrt;
const inv = (x) => 1 / x;
// Compute liquidity constant for the range
const sqrtPi = sqrt(p_i);
const sqrtPa = sqrt(p_a);
const sqrtPb = sqrt(p_b);
const L = depositB / (sqrtPi - sqrtPa);
// Determine token amounts at current price
let amountA;
let amountB;
if (p <= p_a) {
amountA = depositA + depositB / p_a;
amountB = 0;
}
else if (p >= p_b) {
amountA = 0;
amountB = depositB + depositA * p_b;
}
else {
const sqrtP = sqrt(p);
amountA = L * (inv(sqrtP) - inv(sqrtPb));
amountB = L * (sqrtP - sqrtPa);
}
// Compute values
const holdValue = depositA * p + depositB;
const lpValue = amountA * p + amountB;
const totalIL = lpValue / holdValue - 1;
const changeA = amountA - depositA;
const changeB = amountB - depositB;
const ilAPct = depositA ? (changeA / depositA) * 100 : null;
const ilBPct = depositB ? (changeB / depositB) * 100 : null;
return { totalIL, amountA, amountB, changeA, changeB, ilAPct, ilBPct, holdValue, lpValue };
};
exports.divergenceLoss = divergenceLoss;
const getLiquidityInTicks = async ({ poolAddress, rpc }) => {
const [whirlpool, onChainPool] = await Promise.all([(0, exports.fetchOrcaPoolByAddress)(poolAddress), (0, whirlpools_client_1.fetchWhirlpool)(rpc, (0, kit_1.address)(poolAddress))]);
const { tickCurrentIndex } = onChainPool.data;
const { tokenA, tokenB, tickSpacing } = whirlpool;
// compute price window around current price
const priceNow = (0, whirlpools_core_1.tickIndexToPrice)(tickCurrentIndex, tokenA.decimals, tokenB.decimals);
// fetch and sort all tick arrays for this pool
const filter = (0, whirlpools_client_1.fixedTickArrayWhirlpoolFilter)((0, kit_1.address)(whirlpool.address));
const tickArrays = await (0, whirlpools_client_1.fetchAllFixedTickArrayWithFilter)(rpc, filter);
tickArrays.sort((a, b) => a.data.startTickIndex - b.data.startTickIndex);
const data = [];
let liquidity = 0n;
// sweep from left to right using liquidityNet
for (const ta of tickArrays) {
const { startTickIndex, ticks } = ta.data;
for (let i = 0; i < ticks.length; i++) {
const tickIndex = startTickIndex + i * tickSpacing;
const tick = ticks[i];
if (!tick.initialized)
continue;
liquidity += BigInt(tick.liquidityNet);
const price = convertTickToPrice(tickIndex, tokenA.decimals, tokenB.decimals, tickSpacing);
data.push({ tickIndex, price, liquidity: Number(liquidity) });
}
}
return { data, currentPrice: priceNow };
};
exports.getLiquidityInTicks = getLiquidityInTicks;