UNPKG

@orca-so/whirlpool-sdk

Version:

Whirlpool SDK for the Orca protocol.

211 lines (210 loc) 11.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SwapSimulator = exports.AmountSpecified = exports.SwapDirection = exports.MAX_TICK_ARRAY_CROSSINGS = void 0; const whirlpool_client_sdk_1 = require("@orca-so/whirlpool-client-sdk"); const anchor_1 = require("@project-serum/anchor"); const tiny_invariant_1 = __importDefault(require("tiny-invariant")); const math_utils_1 = require("../utils/web3/math-utils"); const pool_util_1 = require("../utils/whirlpool/pool-util"); const position_util_1 = require("../utils/public/position-util"); const tick_util_1 = require("../utils/whirlpool/tick-util"); exports.MAX_TICK_ARRAY_CROSSINGS = 2; var SwapDirection; (function (SwapDirection) { SwapDirection["AtoB"] = "Swap A to B"; SwapDirection["BtoA"] = "Swap B to A"; })(SwapDirection = exports.SwapDirection || (exports.SwapDirection = {})); var AmountSpecified; (function (AmountSpecified) { AmountSpecified["Input"] = "Specified input amount"; AmountSpecified["Output"] = "Specified output amount"; })(AmountSpecified = exports.AmountSpecified || (exports.AmountSpecified = {})); class SwapSimulator { constructor() { } // ** METHODS ** simulateSwap(baseInput, input) { return __awaiter(this, void 0, void 0, function* () { const { amountSpecified, swapDirection } = baseInput; let { currentTickIndex, currentLiquidity, amount: specifiedAmountLeft, currentSqrtPriceX64, } = input; (0, tiny_invariant_1.default)(!specifiedAmountLeft.eq(math_utils_1.ZERO), "amount must be nonzero"); let otherAmountCalculated = math_utils_1.ZERO; let tickArraysCrossed = 0; let sqrtPriceLimitX64; while (specifiedAmountLeft.gt(math_utils_1.ZERO)) { if (tickArraysCrossed > exports.MAX_TICK_ARRAY_CROSSINGS) { throw Error("Crossed the maximum number of tick arrays"); } const swapStepSimulationOutput = yield this.simulateSwapStep(baseInput, { sqrtPriceX64: currentSqrtPriceX64, amountRemaining: specifiedAmountLeft, tickIndex: currentTickIndex, liquidity: currentLiquidity, tickArraysCrossed, }); const { input, output, nextSqrtPriceX64, nextTickIndex, hasReachedNextTick } = swapStepSimulationOutput; const [specifiedAmountUsed, otherAmount] = resolveTokenAmounts(input, output, amountSpecified); specifiedAmountLeft = specifiedAmountLeft.sub(specifiedAmountUsed); otherAmountCalculated = otherAmountCalculated.add(otherAmount); if (hasReachedNextTick) { const nextTick = yield fetchTick(baseInput, nextTickIndex); currentLiquidity = calculateNewLiquidity(currentLiquidity, nextTick.liquidityNet, swapDirection); currentTickIndex = swapDirection == SwapDirection.AtoB ? nextTickIndex - 1 : nextTickIndex; } currentSqrtPriceX64 = nextSqrtPriceX64; tickArraysCrossed = swapStepSimulationOutput.tickArraysCrossed; if (tickArraysCrossed > exports.MAX_TICK_ARRAY_CROSSINGS) { sqrtPriceLimitX64 = (0, whirlpool_client_sdk_1.tickIndexToSqrtPriceX64)(nextTickIndex); } } const [inputAmount, outputAmount] = resolveTokenAmounts(input.amount.sub(specifiedAmountLeft), otherAmountCalculated, amountSpecified); if (!sqrtPriceLimitX64) { if (swapDirection === SwapDirection.AtoB) { sqrtPriceLimitX64 = new anchor_1.BN(whirlpool_client_sdk_1.MIN_SQRT_PRICE); } else { sqrtPriceLimitX64 = new anchor_1.BN(whirlpool_client_sdk_1.MAX_SQRT_PRICE); } } // Return sqrtPriceLimit if 3 tick arrays crossed return { amountIn: inputAmount, amountOut: outputAmount, sqrtPriceLimitX64, }; }); } simulateSwapStep(baseInput, input) { return __awaiter(this, void 0, void 0, function* () { const { whirlpoolData, amountSpecified, swapDirection } = baseInput; const { feeRate } = whirlpoolData; const feeRatePercentage = pool_util_1.PoolUtil.getFeeRate(feeRate); const { amountRemaining, liquidity, sqrtPriceX64, tickIndex, tickArraysCrossed } = input; const { tickIndex: nextTickIndex, tickArraysCrossed: tickArraysCrossedUpdate } = // Return last tick in tick array if max tick arrays crossed // Error out of this gets called for another iteration yield getNextInitializedTickIndex(baseInput, tickIndex, tickArraysCrossed); const targetSqrtPriceX64 = (0, whirlpool_client_sdk_1.tickIndexToSqrtPriceX64)(nextTickIndex); let fixedDelta = (0, position_util_1.getAmountFixedDelta)(sqrtPriceX64, targetSqrtPriceX64, liquidity, amountSpecified, swapDirection); let amountCalculated = amountRemaining; if (amountSpecified == AmountSpecified.Input) { amountCalculated = calculateAmountAfterFees(amountRemaining, feeRatePercentage); } const nextSqrtPriceX64 = amountCalculated.gte(fixedDelta) ? targetSqrtPriceX64 // Fully utilize liquidity till upcoming (next/prev depending on swap type) initialized tick : (0, position_util_1.getNextSqrtPrice)(sqrtPriceX64, liquidity, amountCalculated, amountSpecified, swapDirection); const hasReachedNextTick = nextSqrtPriceX64.eq(targetSqrtPriceX64); const unfixedDelta = (0, position_util_1.getAmountUnfixedDelta)(sqrtPriceX64, nextSqrtPriceX64, liquidity, amountSpecified, swapDirection); if (!hasReachedNextTick) { fixedDelta = (0, position_util_1.getAmountFixedDelta)(sqrtPriceX64, nextSqrtPriceX64, liquidity, amountSpecified, swapDirection); } let [inputDelta, outputDelta] = resolveTokenAmounts(fixedDelta, unfixedDelta, amountSpecified); // Cap output if output specified if (amountSpecified == AmountSpecified.Output && outputDelta.gt(amountRemaining)) { outputDelta = amountRemaining; } if (amountSpecified == AmountSpecified.Input && !hasReachedNextTick) { inputDelta = amountRemaining; } else { inputDelta = inputDelta.add(calculateFeesFromAmount(inputDelta, feeRatePercentage)); } return { nextTickIndex, nextSqrtPriceX64, input: inputDelta, output: outputDelta, tickArraysCrossed: tickArraysCrossedUpdate, hasReachedNextTick, }; }); } } exports.SwapSimulator = SwapSimulator; function calculateAmountAfterFees(amount, feeRate) { return amount.mul(feeRate.denominator.sub(feeRate.numerator)).div(feeRate.denominator); } function calculateFeesFromAmount(amount, feeRate) { return (0, math_utils_1.divRoundUp)(amount.mul(feeRate.numerator), feeRate.denominator.sub(feeRate.numerator)); } function calculateNewLiquidity(liquidity, nextLiquidityNet, swapDirection) { if (swapDirection == SwapDirection.AtoB) { nextLiquidityNet = nextLiquidityNet.neg(); } return liquidity.add(nextLiquidityNet); } function resolveTokenAmounts(specifiedTokenAmount, otherTokenAmount, amountSpecified) { if (amountSpecified == AmountSpecified.Input) { return [specifiedTokenAmount, otherTokenAmount]; } else { return [otherTokenAmount, specifiedTokenAmount]; } } function fetchTickArray(baseInput, tickIndex) { return __awaiter(this, void 0, void 0, function* () { const { dal, poolAddress, refresh, whirlpoolData: { tickSpacing }, } = baseInput; const tickArray = yield dal.getTickArray(tick_util_1.TickUtil.getPdaWithTickIndex(tickIndex, tickSpacing, poolAddress, dal.programId).publicKey, refresh); (0, tiny_invariant_1.default)(!!tickArray, "tickArray is null"); return tickArray; }); } function fetchTick(baseInput, tickIndex) { return __awaiter(this, void 0, void 0, function* () { const tickArray = yield fetchTickArray(baseInput, tickIndex); const { whirlpoolData: { tickSpacing }, } = baseInput; return tick_util_1.TickUtil.getTick(tickArray, tickIndex, tickSpacing); }); } function getNextInitializedTickIndex(baseInput, currentTickIndex, tickArraysCrossed) { return __awaiter(this, void 0, void 0, function* () { const { whirlpoolData: { tickSpacing }, swapDirection, } = baseInput; let nextInitializedTickIndex = undefined; while (nextInitializedTickIndex === undefined) { const currentTickArray = yield fetchTickArray(baseInput, currentTickIndex); let temp; if (swapDirection == SwapDirection.AtoB) { temp = tick_util_1.TickUtil.getPrevInitializedTickIndex(currentTickArray, currentTickIndex, tickSpacing); } else { temp = tick_util_1.TickUtil.getNextInitializedTickIndex(currentTickArray, currentTickIndex, tickSpacing); } if (temp) { nextInitializedTickIndex = temp; } else if (tickArraysCrossed === exports.MAX_TICK_ARRAY_CROSSINGS) { if (swapDirection === SwapDirection.AtoB) { nextInitializedTickIndex = currentTickArray.startTickIndex; } else { nextInitializedTickIndex = currentTickArray.startTickIndex + whirlpool_client_sdk_1.TICK_ARRAY_SIZE * tickSpacing; } tickArraysCrossed++; } else { if (swapDirection === SwapDirection.AtoB) { currentTickIndex = currentTickArray.startTickIndex - 1; } else { currentTickIndex = currentTickArray.startTickIndex + whirlpool_client_sdk_1.TICK_ARRAY_SIZE * tickSpacing; } tickArraysCrossed++; } } return { tickIndex: nextInitializedTickIndex, tickArraysCrossed, }; }); }