UNPKG

@raydium-io/raydium-sdk-v2

Version:

An SDK for building applications on top of Raydium.

261 lines (232 loc) 9.42 kB
import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { ClmmConfigLayout, PoolInfoLayout, TickArrayBitmapExtensionLayout, TickArrayLayout } from "../layout"; import { BN_ZERO, MAX_SQRT_PRICE_X64, MIN_SQRT_PRICE_X64 } from "./constants"; import { LiquidityMathUtil } from "./liquidityMath"; import { PoolUtil } from "./pool"; import { SwapMathUtil, SwapState } from "./swapMath"; import { TickArrayBitmapUtil, TickArrayUtil, TickUtil } from "./tickArrayUtil"; export interface SwapSimulationResult { allTrade: boolean; amountSpecifiedRemaining: BN; amountCalculated: BN; feeAmount: BN; sqrtPriceX64: BN; liquidity: BN; tickCurrent: number; accounts: PublicKey[]; } export function swapInternal({ programId, poolId, poolInfo, tickArrays, configInfo, tickarrayBitmapExtension, amountSpecified, sqrtPriceLimitX64, zeroForOne, isBaseInput, blockTimestamp, includeExtraTickArrays, }: { programId: PublicKey; poolId: PublicKey; poolInfo: ReturnType<typeof PoolInfoLayout.decode>; tickArrays: { address: PublicKey; value: ReturnType<typeof TickArrayLayout.decode> }[]; configInfo: ReturnType<typeof ClmmConfigLayout.decode>; tickarrayBitmapExtension: ReturnType<typeof TickArrayBitmapExtensionLayout.decode>; amountSpecified: BN; sqrtPriceLimitX64: BN; zeroForOne: boolean; isBaseInput: boolean; blockTimestamp: number; includeExtraTickArrays: boolean; }): SwapSimulationResult { if (sqrtPriceLimitX64.isZero()) { sqrtPriceLimitX64 = zeroForOne ? new BN(MIN_SQRT_PRICE_X64).addn(1) : new BN(MAX_SQRT_PRICE_X64).subn(1); } let tickArrayListIndex = 0; if (tickArrays.length === 0) { return { allTrade: false, amountSpecifiedRemaining: amountSpecified, amountCalculated: BN_ZERO, feeAmount: BN_ZERO, sqrtPriceX64: poolInfo.sqrtPriceX64, liquidity: poolInfo.liquidity, tickCurrent: poolInfo.tickCurrent, accounts: [], }; } const addTickArrayAddress = includeExtraTickArrays ? TickArrayBitmapUtil.findTickArrayAddress({ programId, poolId, tickSpacing: poolInfo.tickSpacing, poolBitmap: poolInfo.tickArrayBitmap, tickArrayBitmap: tickarrayBitmapExtension, findInfo: { type: !zeroForOne ? "zeroForOne" : "oneForZero", count: 2, tickArrayCurrent: poolInfo.tickCurrent }, }).filter((i) => i.toString() !== tickArrays[0].address.toString()) : []; const _startTickIndex = TickArrayUtil.getTickArrayStartIndex(poolInfo.tickCurrent, poolInfo.tickSpacing); const { firstItckArrayContainsPoolTick: _firstItckArrayContainsPoolTick, firstValidTickArrayStartIndex } = { firstItckArrayContainsPoolTick: tickArrays[tickArrayListIndex].value.startTickIndex === _startTickIndex, firstValidTickArrayStartIndex: tickArrays[tickArrayListIndex].value.startTickIndex, }; let firstItckArrayContainsPoolTick = _firstItckArrayContainsPoolTick; let currentValidTIckArrayStrartIndex = firstValidTickArrayStartIndex; let tickArrayCurrent = tickArrays[tickArrayListIndex]; const isFeeOnInput = PoolUtil.isFeeOnInput(poolInfo.feeOn, zeroForOne); const state = SwapState.newValue({ poolInfo, amountSpecified, zeroForOne, feeRate: configInfo.tradeFeeRate, blockTimestamp, }); while (!state.amountSpecifiedRemaining.isZero() && !state.sqrtPriceX64.eq(sqrtPriceLimitX64)) { const nextInitializedTick = (() => { const tickState = TickArrayUtil.nextInitalizedTick({ data: tickArrayCurrent.value, tickSpacing: state.tickSpacing, zeroForOne, currentTickIndex: state.tick, }); if (tickState !== undefined) { return tickState; } else if (!firstItckArrayContainsPoolTick) { firstItckArrayContainsPoolTick = true; return TickArrayUtil.firstinitializedTick({ data: tickArrayCurrent.value, zeroForOne }); } else { const nextTickArrayIndex = tickArrays[++tickArrayListIndex]; if (nextTickArrayIndex === undefined) { return undefined; } currentValidTIckArrayStrartIndex = nextTickArrayIndex.value.startTickIndex; tickArrayCurrent = nextTickArrayIndex; return TickArrayUtil.firstinitializedTick({ data: nextTickArrayIndex.value, zeroForOne }); } })(); if (nextInitializedTick === undefined) { return { allTrade: false, amountSpecifiedRemaining: state.amountSpecifiedRemaining, amountCalculated: state.amountCalculated, feeAmount: state.lpFee.add(state.fundFee).add(state.protocolFee), sqrtPriceX64: state.sqrtPriceX64, liquidity: state.liquidity, tickCurrent: state.tick, accounts: tickArrays.slice(0, tickArrayListIndex).map((i) => i.address), }; } const targetPrice = SwapState.getTargetPriceBasedOnNextTick({ data: state, tickNext: nextInitializedTick.tick, zeroForOne, sqrtPriceLimitX64, }); let liquidityNext = state.liquidity; do { SwapState.updateVolatilityAccumulator({ state }); const totalFeeRate = SwapState.getTotalFeeRate({ data: state }); const { isSkipped: isSkippedTickSpacing, boundedPrice } = SwapState.getSpacingBoundedPrice({ data: state, targetPrice, zeroForOne, }); const isPriceChange = !state.sqrtPriceX64.eq(boundedPrice); let swapComputedResult; if (isPriceChange) { swapComputedResult = SwapMathUtil.computeSwap( state.sqrtPriceX64, boundedPrice, state.liquidity, state.amountSpecifiedRemaining, totalFeeRate, isBaseInput, zeroForOne, isFeeOnInput, ); SwapState.applySwapAmounts({ state, amountIn: swapComputedResult.amountIn, amountOut: swapComputedResult.amountOut, feeAmount: swapComputedResult.feeAmount, isBaseInput, isFeeOnInput, protocolFeeRate: new BN(configInfo.protocolFeeRate), fundFeeRate: new BN(configInfo.fundFeeRate), }); } else { swapComputedResult = SwapMathUtil.newSwapComputationResult({ sqrtPriceNextX64: boundedPrice }); } const limitOrderUnfilledAmountBefore = TickUtil.limitOrderUnfilledAmount({ tick: nextInitializedTick }); if (state.sqrtPriceNextX64.eq(swapComputedResult.sqrtPriceNextX64)) { const limitOrderResult = TickUtil.matchLimitOrder({ tick: nextInitializedTick, swapAmount: state.amountSpecifiedRemaining, swapDirectionZeroForOne: zeroForOne, isBaseInput, feeRate: totalFeeRate, isFeeOnInput, }); if (limitOrderResult.amountIn.gt(BN_ZERO)) { SwapState.applySwapAmounts({ state, amountIn: limitOrderResult.amountIn, amountOut: limitOrderResult.amountOut, feeAmount: limitOrderResult.ammFeeAmount, isBaseInput, isFeeOnInput, protocolFeeRate: new BN(configInfo.protocolFeeRate), fundFeeRate: new BN(configInfo.fundFeeRate), }); } if ( TickUtil.hasLiquidity({ data: nextInitializedTick }) && !TickUtil.hasLimitOrders({ data: nextInitializedTick }) ) { const liquidityNet = zeroForOne ? nextInitializedTick.liquidityNet.neg() : nextInitializedTick.liquidityNet; liquidityNext = LiquidityMathUtil.addDelta(state.liquidity, liquidityNet); } state.tick = (zeroForOne && !TickUtil.hasLimitOrders({ data: nextInitializedTick })) || (!zeroForOne && TickUtil.hasLimitOrders({ data: nextInitializedTick })) ? state.tickNext - 1 : state.tickNext; } else if (!state.sqrtPriceX64.eq(swapComputedResult.sqrtPriceNextX64)) { state.tick = TickUtil.getTickAtSqrtPrice(swapComputedResult.sqrtPriceNextX64); } state.sqrtPriceX64 = swapComputedResult.sqrtPriceNextX64; SwapState.updateDynamicFeeIndex({ state, zeroForOne, isSkippedTickSpacing }); if (state.amountSpecifiedRemaining.isZero() || state.sqrtPriceX64.eq(targetPrice)) { const limitOrderUnfilledAmountAfter = TickUtil.limitOrderUnfilledAmount({ tick: nextInitializedTick }); if ( !state.amountSpecifiedRemaining.isZero() && !limitOrderUnfilledAmountAfter.eq(limitOrderUnfilledAmountBefore) ) { if (!limitOrderUnfilledAmountAfter.isZero()) throw Error("!limitOrderUnfilledAmountAfter.isZero()"); } break; } // eslint-disable-next-line no-constant-condition } while (true); state.liquidity = liquidityNext; // SwapState.splitFee({ state, protocolFeeRate: new BN(configInfo.protocolFeeRate), fundFeeRate: new BN(configInfo.fundFeeRate) }) } SwapState.updateVolatilityAccumulatorOnPrice({ state }); return { allTrade: true, amountSpecifiedRemaining: BN_ZERO, amountCalculated: state.amountCalculated, feeAmount: state.lpFee.add(state.fundFee).add(state.protocolFee), sqrtPriceX64: state.sqrtPriceX64, liquidity: state.liquidity, tickCurrent: state.tick, accounts: [ ...addTickArrayAddress, ...tickArrays.slice(0, tickArrayListIndex + 1 + (includeExtraTickArrays ? 1 : 0)).map((i) => i.address), ], }; }