import type { Transaction } from '@mysten/sui/dist/cjs/transactions' import BN from 'bn.js' import type { CoinAmounts, LiquidityInput } from '../type/clmm' import { MAX_SQRT_PRICE, MIN_SQRT_PRICE, PositionStatus } from '../type/clmm' import type { Package } from '../type/sui' import Decimal from './decimal' import { d, fromDecimalsAmount, toDecimalsAmount } from './numbers' import { TickMath } from './tickMath' import { MathUtil, ONE, U64_MAX, ZERO } from './utils' import { CommonErrorCode, handleMessageError } from '../errors/errors' import { DETAILS_KEYS } from '../errors/baseError' /** * Get the amount A delta about two prices, for give amount of liquidity. * `delta_a = (liquidity * delta_sqrt_price) / sqrt_price_upper * sqrt_price_lower)` * * @param sqrtPrice0 - A sqrt price * @param sqrtPrice1 - Another sqrt price * @param liquidity - The amount of usable liquidity * @param roundUp - Whether to round the amount up or down * @returns */ export function getDeltaA(sqrtPrice0: BN, sqrtPrice1: BN, liquidity: BN, roundUp: boolean): BN { const sqrtPriceDiff = sqrtPrice0.gt(sqrtPrice1) ? sqrtPrice0.sub(sqrtPrice1) : sqrtPrice1.sub(sqrtPrice0) const numerator = liquidity.mul(sqrtPriceDiff).shln(64) const denominator = sqrtPrice0.mul(sqrtPrice1) const quotient = numerator.div(denominator) const remainder = numerator.mod(denominator) const result = roundUp && !remainder.eq(ZERO) ? quotient.add(new BN(1)) : quotient return result } /** * Get the amount B delta about two prices, for give amount of liquidity. * `delta_a = (liquidity * delta_sqrt_price) / sqrt_price_upper * sqrt_price_lower)` * * @param sqrtPrice0 - A sqrt price * @param sqrtPrice1 - Another sqrt price * @param liquidity - The amount of usable liquidity * @param roundUp - Whether to round the amount up or down * @returns */ export function getDeltaB(sqrtPrice0: BN, sqrtPrice1: BN, liquidity: BN, roundUp: boolean): BN { const sqrtPriceDiff = sqrtPrice0.gt(sqrtPrice1) ? sqrtPrice0.sub(sqrtPrice1) : sqrtPrice1.sub(sqrtPrice0) if (liquidity.eq(ZERO) || sqrtPriceDiff.eq(ZERO)) { return ZERO } const p = liquidity.mul(sqrtPriceDiff) const shouldRoundUp = roundUp && p.and(U64_MAX).gt(ZERO) const result = shouldRoundUp ? p.shrn(64).add(ONE) : p.shrn(64) if (MathUtil.isOverflow(result, 64)) { throw new Error('Result large than u64 max') } return result } /** * Get the next sqrt price from give a delta of token_a. * `new_sqrt_price = (sqrt_price * liquidity) / (liquidity +/- amount * sqrt_price)` * * @param sqrtPrice - The start sqrt price * @param liquidity - The amount of usable liquidity * @param amount - The amount of token_a * @param byAmountIn - Weather to fixed input */ export function getNextSqrtPriceAUp(sqrtPrice: BN, liquidity: BN, amount: BN, byAmountIn: boolean): BN { if (amount.eq(ZERO)) { return sqrtPrice } const numerator = MathUtil.checkMulShiftLeft(sqrtPrice, liquidity, 64, 256) const liquidityShl64 = liquidity.shln(64) const product = MathUtil.checkMul(sqrtPrice, amount, 256) if (!byAmountIn && liquidityShl64.lte(product)) { throw new Error('getNextSqrtPriceAUp - Unable to divide liquidityShl64 by product') } const nextSqrtPrice = byAmountIn ? MathUtil.checkDivRoundUpIf(numerator, liquidityShl64.add(product), true) : MathUtil.checkDivRoundUpIf(numerator, liquidityShl64.sub(product), true) if (nextSqrtPrice.lt(new BN(MIN_SQRT_PRICE))) { throw new Error('getNextSqrtPriceAUp - Next sqrt price less than min sqrt price') } if (nextSqrtPrice.gt(new BN(MAX_SQRT_PRICE))) { throw new Error('getNextSqrtPriceAUp - Next sqrt price greater than max sqrt price') } return nextSqrtPrice } /** * Get the next sqrt price from give a delta of token_b. * `new_sqrt_price = (sqrt_price +(delta_b / liquidity)` * * @param sqrtPrice - The start sqrt price * @param liquidity - The amount of usable liquidity * @param amount - The amount of token_a * @param byAmountIn - Weather to fixed input */ export function getNextSqrtPriceBDown(sqrtPrice: BN, liquidity: BN, amount: BN, byAmountIn: boolean): BN { const deltaSqrtPrice = MathUtil.checkDivRoundUpIf(amount.shln(64), liquidity, !byAmountIn) const nextSqrtPrice = byAmountIn ? sqrtPrice.add(deltaSqrtPrice) : sqrtPrice.sub(deltaSqrtPrice) if (nextSqrtPrice.lt(new BN(MIN_SQRT_PRICE)) || nextSqrtPrice.gt(new BN(MAX_SQRT_PRICE))) { throw new Error('getNextSqrtPriceAUp - Next sqrt price out of bounds') } return nextSqrtPrice } /** * Get next sqrt price from input parameter. * * @param sqrtPrice * @param liquidity * @param amount * @param aToB * @returns */ export function getNextSqrtPriceFromInput(sqrtPrice: BN, liquidity: BN, amount: BN, aToB: boolean): BN { return aToB ? getNextSqrtPriceAUp(sqrtPrice, liquidity, amount, true) : getNextSqrtPriceBDown(sqrtPrice, liquidity, amount, true) } /** * Get the next sqrt price from output parameters. * * @param sqrtPrice * @param liquidity * @param amount * @param a2b * @returns */ export function getNextSqrtPriceFromOutput(sqrtPrice: BN, liquidity: BN, amount: BN, a2b: boolean): BN { return a2b ? getNextSqrtPriceBDown(sqrtPrice, liquidity, amount, false) : getNextSqrtPriceAUp(sqrtPrice, liquidity, amount, false) } /** * Get the amount of delta_a or delta_b from input parameters, and round up result. * * @param currentSqrtPrice * @param targetSqrtPrice * @param liquidity * @param a2b * @returns */ export function getDeltaUpFromInput(currentSqrtPrice: BN, targetSqrtPrice: BN, liquidity: BN, a2b: boolean): BN { const sqrtPriceDiff = currentSqrtPrice.gt(targetSqrtPrice) ? currentSqrtPrice.sub(targetSqrtPrice) : targetSqrtPrice.sub(currentSqrtPrice) if (liquidity.lte(ZERO) || sqrtPriceDiff.eq(ZERO)) { return ZERO } let result if (a2b) { const numerator = new BN(liquidity).mul(new BN(sqrtPriceDiff)).shln(64) const denominator = targetSqrtPrice.mul(currentSqrtPrice) const quotient = numerator.div(denominator) const remainder = numerator.mod(denominator) result = !remainder.eq(ZERO) ? quotient.add(ONE) : quotient } else { const product = new BN(liquidity).mul(new BN(sqrtPriceDiff)) const shouldRoundUp = product.and(U64_MAX).gt(ZERO) result = shouldRoundUp ? product.shrn(64).add(ONE) : product.shrn(64) } return result } /** * Get the amount of delta_a or delta_b from output parameters, and round down result. * * @param currentSqrtPrice * @param targetSqrtPrice * @param liquidity * @param a2b * @returns */ export function getDeltaDownFromOutput(currentSqrtPrice: BN, targetSqrtPrice: BN, liquidity: BN, a2b: boolean): BN { const sqrtPriceDiff = currentSqrtPrice.gt(targetSqrtPrice) ? currentSqrtPrice.sub(targetSqrtPrice) : targetSqrtPrice.sub(currentSqrtPrice) if (liquidity.lte(ZERO) || sqrtPriceDiff.eq(ZERO)) { return ZERO } let result if (a2b) { const product = liquidity.mul(sqrtPriceDiff) result = product.shrn(64) } else { const numerator = liquidity.mul(sqrtPriceDiff).shln(64) const denominator = targetSqrtPrice.mul(currentSqrtPrice) result = numerator.div(denominator) } return result } /** * Estimate liquidity for coin A * @param sqrtPriceX - coin A sqrt price * @param sqrtPriceY - coin B sqrt price * @param coinAmount - token amount * @return */ export function estimateLiquidityForCoinA(sqrtPriceX: BN, sqrtPriceY: BN, coinAmount: BN) { const lowerSqrtPriceX64 = BN.min(sqrtPriceX, sqrtPriceY) const upperSqrtPriceX64 = BN.max(sqrtPriceX, sqrtPriceY) const num = MathUtil.fromX64BN(coinAmount.mul(upperSqrtPriceX64).mul(lowerSqrtPriceX64)) const dem = upperSqrtPriceX64.sub(lowerSqrtPriceX64) return !num.isZero() && !dem.isZero() ? num.div(dem) : new BN(0) } /** * Estimate liquidity for coin B * @param sqrtPriceX - coin A sqrt price * @param sqrtPriceY - coin B sqrt price * @param coinAmount - token amount * @return */ export function estimateLiquidityForCoinB(sqrtPriceX: BN, sqrtPriceY: BN, coinAmount: BN) { const lowerSqrtPriceX64 = BN.min(sqrtPriceX, sqrtPriceY) const upperSqrtPriceX64 = BN.max(sqrtPriceX, sqrtPriceY) const delta = upperSqrtPriceX64.sub(lowerSqrtPriceX64) return !delta.isZero() ? coinAmount.shln(64).div(delta) : new BN(0) } export class ClmmPoolUtil { /** * Get token amount from liquidity. * @param liquidity - liquidity * @param curSqrtPrice - Pool current sqrt price * @param lowerSqrtPrice - position lower sqrt price * @param upperSqrtPrice - position upper sqrt price * @param roundUp - is round up * @returns */ static getCoinAmountFromLiquidity( liquidity: BN, curSqrtPrice: BN, lowerSqrtPrice: BN, upperSqrtPrice: BN, roundUp: boolean ): CoinAmounts { const liq = new Decimal(liquidity.toString()) const curSqrtPriceStr = new Decimal(curSqrtPrice.toString()) const lowerPriceStr = new Decimal(lowerSqrtPrice.toString()) const upperPriceStr = new Decimal(upperSqrtPrice.toString()) let coinA let coinB if (curSqrtPrice.lt(lowerSqrtPrice)) { coinA = MathUtil.toX64Decimal(liq).mul(upperPriceStr.sub(lowerPriceStr)).div(lowerPriceStr.mul(upperPriceStr)) coinB = new Decimal(0) } else if (curSqrtPrice.lt(upperSqrtPrice)) { coinA = MathUtil.toX64Decimal(liq).mul(upperPriceStr.sub(curSqrtPriceStr)).div(curSqrtPriceStr.mul(upperPriceStr)) coinB = MathUtil.fromX64Decimal(liq.mul(curSqrtPriceStr.sub(lowerPriceStr))) } else { coinA = new Decimal(0) coinB = MathUtil.fromX64Decimal(liq.mul(upperPriceStr.sub(lowerPriceStr))) } if (roundUp) { return { coin_amount_a: coinA.ceil().toString(), coin_amount_b: coinB.ceil().toString(), } } return { coin_amount_a: coinA.floor().toString(), coin_amount_b: coinB.floor().toString(), } } /** * Estimate liquidity and token amount from one amounts * @param lowerTick - lower tick * @param upperTick - upper tick * @param coinAmount - token amount * @param isCoinA - is token A * @param roundUp - is round up * @param isIncrease - is increase * @param slippage - slippage percentage * @param curSqrtPrice - current sqrt price. * @return IncreaseLiquidityInput */ static estLiquidityAndCoinAmountFromOneAmounts( lowerTick: number, upperTick: number, coinAmount: BN, isCoinA: boolean, roundUp: boolean, slippage: number, curSqrtPrice: BN ): LiquidityInput { const currentTick = TickMath.sqrtPriceX64ToTickIndex(curSqrtPrice) const lowerSqrtPrice = TickMath.tickIndexToSqrtPriceX64(lowerTick) const upperSqrtPrice = TickMath.tickIndexToSqrtPriceX64(upperTick) let liquidity if (currentTick < lowerTick) { if (!isCoinA) { throw new Error('lower tick cannot calculate liquidity by coinB') } liquidity = estimateLiquidityForCoinA(lowerSqrtPrice, upperSqrtPrice, coinAmount) } else if (currentTick > upperTick) { if (isCoinA) { throw new Error('upper tick cannot calculate liquidity by coinA') } liquidity = estimateLiquidityForCoinB(upperSqrtPrice, lowerSqrtPrice, coinAmount) } else if (isCoinA) { liquidity = estimateLiquidityForCoinA(curSqrtPrice, upperSqrtPrice, coinAmount) } else { liquidity = estimateLiquidityForCoinB(curSqrtPrice, lowerSqrtPrice, coinAmount) } const coinAmounts = ClmmPoolUtil.getCoinAmountFromLiquidity(liquidity, curSqrtPrice, lowerSqrtPrice, upperSqrtPrice, roundUp) const tokenLimitA = roundUp ? d(coinAmounts.coin_amount_a.toString()) .mul(1 + slippage) .toString() : d(coinAmounts.coin_amount_a.toString()) .mul(1 - slippage) .toString() const tokenLimitB = roundUp ? d(coinAmounts.coin_amount_b.toString()) .mul(1 + slippage) .toString() : d(coinAmounts.coin_amount_b.toString()) .mul(1 - slippage) .toString() return { coin_amount_a: coinAmounts.coin_amount_a, coin_amount_b: coinAmounts.coin_amount_b, coin_amount_limit_a: roundUp ? Decimal.ceil(tokenLimitA).toString() : Decimal.floor(tokenLimitA).toString(), coin_amount_limit_b: roundUp ? Decimal.ceil(tokenLimitB).toString() : Decimal.floor(tokenLimitB).toString(), liquidity_amount: liquidity.toString(), fix_amount_a: isCoinA, } } /** * Estimate liquidity from token amounts * @param curSqrtPrice - current sqrt price. * @param lowerTick - lower tick * @param upperTick - upper tick * @param tokenAmount - token amount * @return */ static estimateLiquidityFromCoinAmounts(curSqrtPrice: BN, lowerTick: number, upperTick: number, tokenAmount: CoinAmounts): string { if (lowerTick > upperTick) { return handleMessageError(CommonErrorCode.InvalidTick, 'lower tick cannot be greater than lower tick', { [DETAILS_KEYS.METHOD_NAME]: 'estimateLiquidityFromCoinAmounts', [DETAILS_KEYS.REQUEST_PARAMS]: { lowerTick, upperTick, tokenAmount, }, }) } const currTick = TickMath.sqrtPriceX64ToTickIndex(curSqrtPrice) const lowerSqrtPrice = TickMath.tickIndexToSqrtPriceX64(lowerTick) const upperSqrtPrice = TickMath.tickIndexToSqrtPriceX64(upperTick) if (currTick < lowerTick) { return estimateLiquidityForCoinA(lowerSqrtPrice, upperSqrtPrice, new BN(tokenAmount.coin_amount_a)).toString() } if (currTick >= upperTick) { return estimateLiquidityForCoinB(upperSqrtPrice, lowerSqrtPrice, new BN(tokenAmount.coin_amount_b)).toString() } const estimateLiquidityAmountA = estimateLiquidityForCoinA(curSqrtPrice, upperSqrtPrice, new BN(tokenAmount.coin_amount_a)) const estimateLiquidityAmountB = estimateLiquidityForCoinB(curSqrtPrice, lowerSqrtPrice, new BN(tokenAmount.coin_amount_b)) return BN.min(estimateLiquidityAmountA, estimateLiquidityAmountB).toString() } static calculateDepositRatio(lowerTick: number, upperTick: number, curSqrtPrice: BN) { // Use a fixed amount of token A with proper decimals const coinAmountA = new BN(100000000) const { coin_amount_b } = ClmmPoolUtil.estLiquidityAndCoinAmountFromOneAmounts( lowerTick, upperTick, coinAmountA, true, true, 0, curSqrtPrice ) const currPrice = TickMath.sqrtPriceX64ToPrice(curSqrtPrice, 0, 0) const transformAmountB = d(coinAmountA.toString()).mul(currPrice) const totalAmount = transformAmountB.add(coin_amount_b.toString()) const ratioA = transformAmountB.div(totalAmount) const ratioB = d(coin_amount_b.toString()).div(totalAmount) return { ratioA, ratioB } } static calculateAmountDepositRatio( lowerTick: number, upperTick: number, curSqrtPrice: BN, coin_decimal_a: number, coin_decimal_b: number ) { const currPrice = TickMath.sqrtPriceX64ToPrice(curSqrtPrice, coin_decimal_a, coin_decimal_b) const currentTick = TickMath.sqrtPriceX64ToTickIndex(curSqrtPrice) if (currentTick < lowerTick) { return { ratioA: new Decimal(1), ratioB: new Decimal(0), currPrice } } if (currentTick > upperTick) { return { ratioA: new Decimal(0), ratioB: new Decimal(1), currPrice } } const coinAmountA = new BN(toDecimalsAmount(1, coin_decimal_a)) const { coin_amount_b: coinAmountB } = ClmmPoolUtil.estLiquidityAndCoinAmountFromOneAmounts( lowerTick, upperTick, coinAmountA, true, true, 0, curSqrtPrice ) const amountA = fromDecimalsAmount(coinAmountA.toString(), coin_decimal_a) const amountB = fromDecimalsAmount(coinAmountB.toString(), coin_decimal_b) const totalAmount = d(amountA).add(amountB) const ratioA = d(amountA).div(totalAmount) const ratioB = d(amountB).div(totalAmount) return { ratioA, ratioB, currPrice } } static getCoinAmountsFromRatio( ratioA: Decimal, ratioB: Decimal, totalValue: string, tokenPriceA: string, tokenPriceB: string, decimalsA: number, decimalsB: number ) { const amountA = d(toDecimalsAmount(d(totalValue).mul(ratioA).div(tokenPriceA).toString(), decimalsA)).toFixed(0) const amountB = d(toDecimalsAmount(d(totalValue).mul(ratioB).div(tokenPriceB).toString(), decimalsB)).toFixed(0) return { amountA, amountB } } /** * Estimate coin amounts from total amount * @param lowerTick * @param upperTick * @param decimalsA * @param decimalsB * @param curSqrtPrice * @param totalAmount * @param tokenPriceA * @param tokenPriceB * @returns */ static estCoinAmountsFromTotalAmount( lowerTick: number, upperTick: number, curSqrtPrice: BN, totalValue: string, tokenPriceA: string, tokenPriceB: string, decimalsA: number, decimalsB: number ) { const { ratioA, ratioB } = ClmmPoolUtil.calculateDepositRatio(lowerTick, upperTick, curSqrtPrice) return ClmmPoolUtil.getCoinAmountsFromRatio(ratioA, ratioB, totalValue, tokenPriceA, tokenPriceB, decimalsA, decimalsB) } /** * Get the position status for the given tick indices. * * @param currentTickIndex The current tick index. * @param lowerTickIndex The lower tick index. * @param upperTickIndex The upper tick index. * @returns The position status. */ static getPositionStatus(currentTickIndex: number, lowerTickIndex: number, upperTickIndex: number): PositionStatus { if (currentTickIndex < lowerTickIndex) { return PositionStatus.BelowRange } if (currentTickIndex < upperTickIndex) { return PositionStatus.InRange return PositionStatus.InRange } return PositionStatus.AboveRange } } /** * Utility function to retrieve packager configurations from a package object. * @param {Package} packageObj - The package object containing configurations. * @throws {Error} Throws an error if the package does not have a valid config. * @returns {T} The retrieved configuration. */ export function getPackagerConfigs>(packageObj: T): T extends Package ? C : never { if (packageObj.config === undefined) { throw new Error(`package: ${packageObj.package_id} not config in sdk SdkOptions`) } return packageObj.config as T extends Package ? C : never } export async function printTransaction(tx: Transaction, isPrint = true) { console.log(`inputs`, tx.getData().inputs) tx.getData().commands.forEach((item, index) => { if (isPrint) { console.log(`transaction ${index}: `, JSON.stringify(item, null, 2)) } }) }