@raydium-io/raydium-sdk-v2
Version:
An SDK for building applications on top of Raydium.
261 lines (232 loc) • 9.42 kB
text/typescript
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),
],
};
}