@orca-so/whirlpool-sdk
Version:
Whirlpool SDK for the Orca protocol.
211 lines (210 loc) • 11.4 kB
JavaScript
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,
};
});
}
;