UNPKG

@raydium-io/raydium-sdk-v2

Version:

An SDK for building applications on top of Raydium.

390 lines (337 loc) 14.5 kB
import { FEE_RATE_DENOMINATOR_VALUE } from "@/common" import BN from "bn.js" import { DynamicFeeInfoLayout, PoolInfoLayout } from "../layout" import { mulDivCeil, mulDivFloor } from "./bigNum" import { BN_ZERO, DYNAMIC_FEE_CONTROL_DENOMINATOR, FEE_RATE_DENOMINATOR, MAX_FEE_RATE_NUMERATOR, MAX_TICK, MIN_TICK, Q64, VOLATILITY_ACCUMULATOR_SCALE } from "./constants" import { LiquidityMathUtil } from "./liquidityMath" import { DynamicFeeInfo, PoolFee } from "./pool" import { SqrtPriceMath } from "./sqrtPriceMath" import { TickUtil } from "./tickArrayUtil" export interface SwapStepResult { sqrtPriceNextX64: BN amountIn: BN amountOut: BN feeAmount: BN } interface SwapStateInterface { amountSpecifiedRemaining: BN, amountCalculated: BN, sqrtPriceX64: BN, tick: number, feeGrowthGlobalX64: BN, lpFee: BN, protocolFee: BN, fundFee: BN, liquidity: BN, sqrtPriceNextX64: BN, tickNext: number, baseFeeRate: number, tickSpacing: number, tickSpacingIndex: number, dynamicFeeInfo: ReturnType<typeof DynamicFeeInfoLayout.decode> | undefined, } export class SwapState { static newValue({ poolInfo, amountSpecified, zeroForOne, feeRate, blockTimestamp }: { poolInfo: ReturnType<typeof PoolInfoLayout.decode>, amountSpecified: BN, zeroForOne: boolean, feeRate: number, blockTimestamp: number, }): SwapStateInterface { const state: SwapStateInterface = { amountSpecifiedRemaining: amountSpecified, amountCalculated: BN_ZERO, sqrtPriceX64: poolInfo.sqrtPriceX64, tick: poolInfo.tickCurrent, feeGrowthGlobalX64: zeroForOne ? poolInfo.feeGrowthGlobalX64A : poolInfo.feeGrowthGlobalX64B, lpFee: BN_ZERO, protocolFee: BN_ZERO, fundFee: BN_ZERO, liquidity: poolInfo.liquidity, sqrtPriceNextX64: BN_ZERO, tickNext: 0, baseFeeRate: feeRate, tickSpacing: poolInfo.tickSpacing, tickSpacingIndex: 0, dynamicFeeInfo: DynamicFeeInfo.getDynamicFeeInfo({ poolInfo }), } if (state.dynamicFeeInfo) { state.tickSpacingIndex = PoolFee.tickSpacingIndexFromTick(state.tick, state.tickSpacing) DynamicFeeInfo.updateReference({ dynamicFeeInfo: state.dynamicFeeInfo, tickSpacingIndex: state.tickSpacingIndex, currentTimestamp: blockTimestamp }) } return state } static getTargetPriceBasedOnNextTick({ data, tickNext, zeroForOne, sqrtPriceLimitX64 }: { data: SwapStateInterface, tickNext: number, zeroForOne: boolean, sqrtPriceLimitX64: BN, }) { data.tickNext = tickNext if (data.tickNext < MIN_TICK) { data.tickNext = MIN_TICK } else if (data.tickNext > MAX_TICK) { data.tickNext = MAX_TICK } data.sqrtPriceNextX64 = TickUtil.getSqrtPriceAtTick(data.tickNext) let targetPrice: BN if ((zeroForOne && data.sqrtPriceNextX64.lt(sqrtPriceLimitX64)) || (!zeroForOne && data.sqrtPriceNextX64.gt(sqrtPriceLimitX64))) { targetPrice = sqrtPriceLimitX64 } else { targetPrice = data.sqrtPriceNextX64 } if (zeroForOne) { if (data.tick < data.tickNext) throw Error('data.tick < data.tickNext') if (data.sqrtPriceX64.lt(data.sqrtPriceNextX64)) throw Error('data.sqrtPriceX64.lt(data.sqrtPriceNextX64)') if (data.sqrtPriceX64.lt(targetPrice)) throw Error('data.sqrtPriceX64.lt(targetPrice)') } else { if (data.tickNext <= data.tick) throw Error('data.tickNext <= data.tick') if (data.sqrtPriceNextX64.lt(data.sqrtPriceX64)) throw Error('data.sqrtPriceNextX64.lt(data.sqrtPriceX64)') if (targetPrice.lt(data.sqrtPriceX64)) throw Error('targetPrice.lt(data.sqrtPriceX64)') } return targetPrice } static updateVolatilityAccumulator({ state }: { state: SwapStateInterface, }) { if (!state.dynamicFeeInfo) return DynamicFeeInfo.updateVolatilityAccumulator({ state: state.dynamicFeeInfo, tickSpacingIndex: state.tickSpacingIndex }) } static computeDynamicFeeRate({ data, tickSpacing }: { data: ReturnType<typeof DynamicFeeInfoLayout.decode>, tickSpacing: number, }) { const crossed = data.volatilityAccumulator * tickSpacing const squared = crossed * crossed const denominator = DYNAMIC_FEE_CONTROL_DENOMINATOR * VOLATILITY_ACCUMULATOR_SCALE * VOLATILITY_ACCUMULATOR_SCALE const feeRate = mulDivCeil(new BN(data.dynamicFeeControl), new BN(squared), new BN(denominator)).toNumber() if (feeRate > MAX_FEE_RATE_NUMERATOR) { return MAX_FEE_RATE_NUMERATOR } else { return feeRate } } static getTotalFeeRate({ data }: { data: SwapStateInterface, }) { if (data.dynamicFeeInfo) { const dynamicFeeRate = this.computeDynamicFeeRate({ data: data.dynamicFeeInfo, tickSpacing: data.tickSpacing }) const totalFeeRate = data.baseFeeRate + dynamicFeeRate return Math.min(MAX_FEE_RATE_NUMERATOR, totalFeeRate) } return data.baseFeeRate } static getSpacingBoundedPrice({ data, targetPrice, zeroForOne }: { data: SwapStateInterface, targetPrice: BN, zeroForOne: boolean }) { if (data.dynamicFeeInfo === undefined) return { isSkipped: true, boundedPrice: targetPrice } if (data.liquidity.isZero() || data.dynamicFeeInfo.volatilityAccumulator === data.dynamicFeeInfo.maxVolatilityAccumulator) return { isSkipped: true, boundedPrice: targetPrice } const tickSpacingI32 = data.tickSpacing const boundedTick = zeroForOne ? data.tickSpacingIndex * tickSpacingI32 : (data.tickSpacingIndex + 1) * tickSpacingI32 const clampedTick = Math.max(MIN_TICK, Math.min(MAX_TICK, boundedTick)) const boundedSqrtPrice = TickUtil.getSqrtPriceAtTick(clampedTick) if (zeroForOne) { return { isSkipped: false, boundedPrice: BN.max(targetPrice, boundedSqrtPrice) } } else { return { isSkipped: false, boundedPrice: BN.min(targetPrice, boundedSqrtPrice) } } } static applySwapAmounts({ state, amountIn, amountOut, feeAmount, isBaseInput, isFeeOnInput, protocolFeeRate, fundFeeRate, }: { state: SwapStateInterface, amountIn: BN, amountOut: BN, feeAmount: BN, isBaseInput: boolean, isFeeOnInput: boolean, protocolFeeRate: BN, fundFeeRate: BN, }) { const amountInConsumed = isFeeOnInput ? amountIn.add(feeAmount) : amountIn if (isBaseInput) { state.amountSpecifiedRemaining = state.amountSpecifiedRemaining.sub(amountInConsumed) state.amountCalculated = state.amountCalculated.add(amountOut) } else { state.amountSpecifiedRemaining = state.amountSpecifiedRemaining.sub(amountOut) state.amountCalculated = state.amountCalculated.add(amountInConsumed) } this.splitFee({ state, feeAmount, protocolFeeRate, fundFeeRate }) } static updateDynamicFeeIndex({ state, zeroForOne, isSkippedTickSpacing }: { state: SwapStateInterface, zeroForOne: boolean, isSkippedTickSpacing: boolean, }) { if (state.dynamicFeeInfo === undefined) return if (isSkippedTickSpacing) { const tickIndex = state.sqrtPriceX64.eq(state.sqrtPriceNextX64) ? state.tickNext : state.tick let tickSpacingIndex = PoolFee.tickSpacingIndexFromTick(tickIndex, state.tickSpacing) if (!zeroForOne && tickIndex % state.tickSpacing === 0) { tickSpacingIndex = tickSpacingIndex - 1 } state.tickSpacingIndex = tickSpacingIndex if (state.dynamicFeeInfo.volatilityAccumulator !== state.dynamicFeeInfo.maxVolatilityAccumulator) { this.updateVolatilityAccumulator({ state }) } } state.tickSpacingIndex += zeroForOne ? -1 : 1 } static splitFee({ state, feeAmount, protocolFeeRate, fundFeeRate }: { state: SwapStateInterface, feeAmount: BN, protocolFeeRate: BN, fundFeeRate: BN }) { let remainingFee = feeAmount if (protocolFeeRate.gt(BN_ZERO)) { const protocolFeeDelta = feeAmount.mul(protocolFeeRate).div(new BN(FEE_RATE_DENOMINATOR_VALUE)) state.protocolFee = state.protocolFee.add(protocolFeeDelta) remainingFee = remainingFee.sub(protocolFeeDelta) } if (fundFeeRate.gt(BN_ZERO)) { const fundFeeDelta = feeAmount.mul(fundFeeRate).div(new BN(FEE_RATE_DENOMINATOR_VALUE)) state.fundFee = state.fundFee.add(fundFeeDelta) remainingFee = remainingFee.sub(fundFeeDelta) } if (state.liquidity.gt(BN_ZERO)) { const feeGrowthGlobalX64Delta = mulDivFloor(remainingFee, Q64, state.liquidity) state.feeGrowthGlobalX64 = state.feeGrowthGlobalX64.add(feeGrowthGlobalX64Delta) state.lpFee = state.lpFee.add(remainingFee) } } static updateVolatilityAccumulatorOnPrice({ state }: { state: SwapStateInterface, }) { if (state.dynamicFeeInfo) { const tickIndex = TickUtil.getTickAtSqrtPrice(state.sqrtPriceX64) const finalTickSpacingIndex = PoolFee.tickSpacingIndexFromTick(tickIndex, state.tickSpacing) if (state.tickSpacingIndex != finalTickSpacingIndex) { state.tickSpacingIndex = finalTickSpacingIndex this.updateVolatilityAccumulator({ state }) } } } } export class SwapMathUtil { static newSwapComputationResult({ sqrtPriceNextX64 }: { sqrtPriceNextX64?: BN }): SwapStepResult { return { sqrtPriceNextX64: sqrtPriceNextX64 ?? BN_ZERO, amountIn: BN_ZERO, amountOut: BN_ZERO, feeAmount: BN_ZERO, } } static calculateAmountInRange({ sqrtPriceCurrentX64, sqrtPriceTargetX64, liquidity, zeroForOne, isBaseInput }: { sqrtPriceCurrentX64: BN, sqrtPriceTargetX64: BN, liquidity: BN, zeroForOne: boolean, isBaseInput: boolean, }) { if (isBaseInput) { try { const result = zeroForOne ? LiquidityMathUtil.getDeltaAmountAUnsigned(sqrtPriceTargetX64, sqrtPriceCurrentX64, liquidity, true) : LiquidityMathUtil.getDeltaAmountBUnsigned(sqrtPriceCurrentX64, sqrtPriceTargetX64, liquidity, true) return result } catch (e) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (e.message === 'MaxTokenOverflow') return null throw e } } else { try { const result = zeroForOne ? LiquidityMathUtil.getDeltaAmountBUnsigned(sqrtPriceTargetX64, sqrtPriceCurrentX64, liquidity, false) : LiquidityMathUtil.getDeltaAmountAUnsigned(sqrtPriceCurrentX64, sqrtPriceTargetX64, liquidity, false) return result } catch (e) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (e.message === 'MaxTokenOverflow') return null throw e } } } static computeSwap( sqrtPriceCurrentX64: BN, sqrtPriceTargetX64: BN, liquidity: BN, amountRemaining: BN, feeRate: number, isBaseInput: boolean, zeroForOne: boolean, isFeeOnInput: boolean ): SwapStepResult { const result = this.newSwapComputationResult({}) if (isBaseInput) { const amountForPriceCalc = isFeeOnInput ? mulDivFloor(amountRemaining, new BN(FEE_RATE_DENOMINATOR - feeRate), new BN(FEE_RATE_DENOMINATOR)) : amountRemaining const amountIn = this.calculateAmountInRange({ sqrtPriceCurrentX64, sqrtPriceTargetX64, liquidity, zeroForOne, isBaseInput }) if (amountIn !== null) result.amountIn = amountIn result.sqrtPriceNextX64 = amountIn !== null && amountForPriceCalc.gte(result.amountIn) ? sqrtPriceTargetX64 : SqrtPriceMath.getNextSqrtPriceFromInput(sqrtPriceCurrentX64, liquidity, amountForPriceCalc, zeroForOne) } else { const amountForPriceCalc = isFeeOnInput ? amountRemaining : mulDivCeil( amountRemaining, new BN(FEE_RATE_DENOMINATOR), new BN(FEE_RATE_DENOMINATOR - feeRate) ) const amountOut = this.calculateAmountInRange({ sqrtPriceCurrentX64, sqrtPriceTargetX64, liquidity, zeroForOne, isBaseInput }) if (amountOut !== null) result.amountOut = amountOut result.sqrtPriceNextX64 = amountOut !== null && amountForPriceCalc.gte(result.amountOut) ? sqrtPriceTargetX64 : SqrtPriceMath.getNextSqrtPriceFromOutput(sqrtPriceCurrentX64, liquidity, amountForPriceCalc, zeroForOne) } if (zeroForOne) { if (!result.sqrtPriceNextX64.gte(sqrtPriceTargetX64)) throw Error('!result.sqrtPriceNextX64.gte(sqrtPriceTargetX64)') } else { if (!sqrtPriceTargetX64.gte(result.sqrtPriceNextX64)) throw Error('!sqrtPriceTargetX64.gte(result.sqrtPriceNextX64)') } const max = sqrtPriceTargetX64.eq(result.sqrtPriceNextX64) if (zeroForOne) { if (!(max && isBaseInput)) { result.amountIn = LiquidityMathUtil.getDeltaAmountAUnsigned(result.sqrtPriceNextX64, sqrtPriceCurrentX64, liquidity, true) } if (!(max && !isBaseInput)) { result.amountOut = LiquidityMathUtil.getDeltaAmountBUnsigned(result.sqrtPriceNextX64, sqrtPriceCurrentX64, liquidity, false) } } else { if (!(max && isBaseInput)) { result.amountIn = LiquidityMathUtil.getDeltaAmountBUnsigned(sqrtPriceCurrentX64, result.sqrtPriceNextX64, liquidity, true) } if (!(max && !isBaseInput)) { result.amountOut = LiquidityMathUtil.getDeltaAmountAUnsigned(sqrtPriceCurrentX64, result.sqrtPriceNextX64, liquidity, false) } } if (isBaseInput) { if (isFeeOnInput) { if (!result.sqrtPriceNextX64.eq(sqrtPriceTargetX64)) { result.feeAmount = amountRemaining.sub(result.amountIn) } else { result.feeAmount = mulDivCeil( result.amountIn, new BN(feeRate), new BN(FEE_RATE_DENOMINATOR - feeRate) ) } } else { result.feeAmount = mulDivCeil(result.amountOut, new BN(feeRate), new BN(FEE_RATE_DENOMINATOR)) result.amountOut = result.amountOut.sub(result.feeAmount) } } else { if (isFeeOnInput) { result.amountOut = BN.min(result.amountOut, amountRemaining) result.feeAmount = mulDivCeil( result.amountIn, new BN(feeRate), new BN(FEE_RATE_DENOMINATOR - feeRate) ) } else { result.feeAmount = mulDivCeil(result.amountOut, new BN(feeRate), new BN(FEE_RATE_DENOMINATOR)) const netOutput = result.amountOut.sub(result.feeAmount) if (netOutput.gt(amountRemaining)) { result.feeAmount = result.amountOut.sub(amountRemaining) result.amountOut = amountRemaining } else { result.amountOut = netOutput } } } return result } }