@alcorexchange/alcor-swap-sdk
Version:
## Installation **npm** ``` npm i @alcorexchange/alcor-swap-sdk ``` **yarn** ``` yarn add @alcorexchange/alcor-swap-sdk ``` ## Usage ### Import:
643 lines (608 loc) • 19.6 kB
text/typescript
import { CurrencyAmount, Price, Percent } from "./fractions";
import { Token } from "./token";
import { BigintIsh, MaxUint64, Q64 } from "../internalConstants";
import JSBI from "jsbi";
import invariant from "tiny-invariant";
import { ZERO } from "../internalConstants";
import { maxLiquidityForAmounts } from "../utils/maxLiquidityForAmounts";
import { tickToPrice } from "../utils/priceTickConversions";
import { SqrtPriceMath } from "../utils/sqrtPriceMath";
import { TickMath } from "../utils/tickMath";
import { encodeSqrtRatioX64 } from "../utils/encodeSqrtRatioX64";
import { Pool } from "./pool";
import { TickLibrary, subIn128 } from "../utils";
interface PositionConstructorArgs {
id: number,
owner: string,
pool: Pool;
tickLower: number;
tickUpper: number;
liquidity: BigintIsh;
feeGrowthInsideALastX64: BigintIsh,
feeGrowthInsideBLastX64: BigintIsh,
feesA: BigintIsh,
feesB: BigintIsh,
}
interface Fees {
feesA: CurrencyAmount<Token>,
feesB: CurrencyAmount<Token>
}
export class Position {
public readonly id: number;
public readonly owner: string;
public readonly pool: Pool;
public readonly tickLower: number;
public readonly tickUpper: number;
public readonly liquidity: JSBI;
public readonly feesA: JSBI;
public readonly feesB: JSBI;
public readonly feeGrowthInsideALastX64: JSBI;
public readonly feeGrowthInsideBLastX64: JSBI;
// cached resuts for the getters
private _tokenAAmount: CurrencyAmount<Token> | null = null;
private _tokenBAmount: CurrencyAmount<Token> | null = null;
private _mintAmounts: Readonly<{ amountA: JSBI; amountB: JSBI }> | null =
null;
/**
* Constructs a position for a given pool with the given liquidity
* @param pool For which pool the liquidity is assigned
* @param liquidity The amount of liquidity that is in the position
* @param lower The lower tick of the position
* @param upper The upper tick of the position
*/
public constructor({
id,
owner,
pool,
liquidity,
tickLower,
tickUpper,
feeGrowthInsideALastX64 = 0,
feeGrowthInsideBLastX64 = 0,
feesA = 0,
feesB = 0,
}: PositionConstructorArgs) {
invariant(tickLower < tickUpper, "TICK_ORDER");
invariant(
tickLower >= TickMath.MIN_TICK && tickLower % pool.tickSpacing === 0,
"TICK_LOWER"
);
invariant(
tickUpper <= TickMath.MAX_TICK && tickUpper % pool.tickSpacing === 0,
"TICK_UPPER"
);
this.id = id;
this.owner = owner;
this.pool = pool;
this.tickLower = tickLower;
this.tickUpper = tickUpper;
this.liquidity = JSBI.BigInt(liquidity);
this.feeGrowthInsideALastX64 = JSBI.BigInt(feeGrowthInsideALastX64);
this.feeGrowthInsideBLastX64 = JSBI.BigInt(feeGrowthInsideBLastX64);
this.feesA = JSBI.BigInt(feesA)
this.feesB = JSBI.BigInt(feesB)
}
public get inRange(): boolean {
return (
this.tickLower < this.pool.tickCurrent &&
this.pool.tickCurrent < this.tickUpper
);
}
/**
* Returns the price of tokenA at the lower tick
*/
public get tokenAPriceLower(): Price<Token, Token> {
return tickToPrice(this.pool.tokenA, this.pool.tokenB, this.tickLower);
}
/**
* Returns the price of tokenA at the upper tick
*/
public get tokenAPriceUpper(): Price<Token, Token> {
return tickToPrice(this.pool.tokenA, this.pool.tokenB, this.tickUpper);
}
/**
* Returns the amount of tokenA that this position's liquidity could be burned for at the current pool price
*/
public get amountA(): CurrencyAmount<Token> {
if (this._tokenAAmount === null) {
if (this.pool.tickCurrent < this.tickLower) {
this._tokenAAmount = CurrencyAmount.fromRawAmount(
this.pool.tokenA,
SqrtPriceMath.getAmountADelta(
TickMath.getSqrtRatioAtTick(this.tickLower),
TickMath.getSqrtRatioAtTick(this.tickUpper),
this.liquidity,
false
)
);
} else if (this.pool.tickCurrent < this.tickUpper) {
this._tokenAAmount = CurrencyAmount.fromRawAmount(
this.pool.tokenA,
SqrtPriceMath.getAmountADelta(
this.pool.sqrtPriceX64,
TickMath.getSqrtRatioAtTick(this.tickUpper),
this.liquidity,
false
)
);
} else {
this._tokenAAmount = CurrencyAmount.fromRawAmount(
this.pool.tokenA,
ZERO
);
}
}
return this._tokenAAmount;
}
/**
* Returns the amount of tokenB that this position's liquidity could be burned for at the current pool price
*/
public get amountB(): CurrencyAmount<Token> {
if (this._tokenBAmount === null) {
if (this.pool.tickCurrent < this.tickLower) {
this._tokenBAmount = CurrencyAmount.fromRawAmount(
this.pool.tokenB,
ZERO
);
} else if (this.pool.tickCurrent < this.tickUpper) {
this._tokenBAmount = CurrencyAmount.fromRawAmount(
this.pool.tokenB,
SqrtPriceMath.getAmountBDelta(
TickMath.getSqrtRatioAtTick(this.tickLower),
this.pool.sqrtPriceX64,
this.liquidity,
false
)
);
} else {
this._tokenBAmount = CurrencyAmount.fromRawAmount(
this.pool.tokenB,
SqrtPriceMath.getAmountBDelta(
TickMath.getSqrtRatioAtTick(this.tickLower),
TickMath.getSqrtRatioAtTick(this.tickUpper),
this.liquidity,
false
)
);
}
}
return this._tokenBAmount;
}
/**
* Returns the lower and upper sqrt ratios if the price 'slips' up to slippage tolerance percentage
* @param slippageTolerance The amount by which the price can 'slip' before the transaction will revert
* @returns The sqrt ratios after slippage
*/
private ratiosAfterSlippage(slippageTolerance: Percent): {
sqrtPriceX64Lower: JSBI;
sqrtPriceX64Upper: JSBI;
} {
const priceLower = this.pool.tokenAPrice.asFraction.multiply(
new Percent(1).subtract(slippageTolerance)
);
const priceUpper = this.pool.tokenAPrice.asFraction.multiply(
slippageTolerance.add(1)
);
let sqrtPriceX64Lower = encodeSqrtRatioX64(
priceLower.numerator,
priceLower.denominator
);
if (JSBI.lessThanOrEqual(sqrtPriceX64Lower, TickMath.MIN_SQRT_RATIO)) {
sqrtPriceX64Lower = JSBI.add(TickMath.MIN_SQRT_RATIO, JSBI.BigInt(1));
}
let sqrtPriceX64Upper = encodeSqrtRatioX64(
priceUpper.numerator,
priceUpper.denominator
);
if (JSBI.greaterThanOrEqual(sqrtPriceX64Upper, TickMath.MAX_SQRT_RATIO)) {
sqrtPriceX64Upper = JSBI.subtract(
TickMath.MAX_SQRT_RATIO,
JSBI.BigInt(1)
);
}
return {
sqrtPriceX64Lower,
sqrtPriceX64Upper,
};
}
/**
* Returns the minimum amounts that must be sent in order to safely mint the amount of liquidity held by the position
* with the given slippage tolerance
* @param slippageTolerance Tolerance of unfavorable slippage from the current price
* @returns The amounts, with slippage
*/
public mintAmountsWithSlippage(
slippageTolerance: Percent
): Readonly<{ amountA: JSBI; amountB: JSBI }> {
// get lower/upper prices
const { sqrtPriceX64Upper, sqrtPriceX64Lower } =
this.ratiosAfterSlippage(slippageTolerance);
// construct counterfactual pools
const poolLower = new Pool({
id: this.pool.id,
active: this.pool.active,
tokenA: this.pool.tokenA,
tokenB: this.pool.tokenB,
fee: this.pool.fee,
sqrtPriceX64: sqrtPriceX64Lower,
liquidity: 0 /* liquidity doesn't matter */,
tickCurrent: TickMath.getTickAtSqrtRatio(sqrtPriceX64Lower),
feeGrowthGlobalAX64: this.feeGrowthInsideALastX64,
feeGrowthGlobalBX64: this.feeGrowthInsideBLastX64,
ticks: this.pool.tickDataProvider
});
const poolUpper = new Pool({
id: this.pool.id,
active: this.pool.active,
tokenA: this.pool.tokenA,
tokenB: this.pool.tokenB,
fee: this.pool.fee,
sqrtPriceX64: sqrtPriceX64Upper,
liquidity: 0 /* liquidity doesn't matter */,
tickCurrent: TickMath.getTickAtSqrtRatio(sqrtPriceX64Upper),
feeGrowthGlobalAX64: this.feeGrowthInsideALastX64,
feeGrowthGlobalBX64: this.feeGrowthInsideBLastX64,
ticks: this.pool.tickDataProvider
});
// because the router is imprecise, we need to calculate the position that will be created (assuming no slippage)
const positionThatWillBeCreated = Position.fromAmounts({
id: this.id,
owner: this.owner,
pool: this.pool,
tickLower: this.tickLower,
tickUpper: this.tickUpper,
...this.mintAmounts, // the mint amounts are what will be passed as calldata
useFullPrecision: false,
feeGrowthInsideALastX64: this.feeGrowthInsideALastX64,
feeGrowthInsideBLastX64: this.feeGrowthInsideBLastX64,
feesA: this.feesA,
feesB: this.feesB,
});
// we want the smaller amounts...
// ...which occurs at the upper price for amountA...
const { amountA } = new Position({
id: this.id,
owner: this.owner,
pool: poolUpper,
liquidity: positionThatWillBeCreated.liquidity,
tickLower: this.tickLower,
tickUpper: this.tickUpper,
feeGrowthInsideALastX64: this.feeGrowthInsideALastX64,
feeGrowthInsideBLastX64: this.feeGrowthInsideBLastX64,
feesA: this.feesA,
feesB: this.feesB,
}).mintAmounts;
// ...and the lower for amountB
const { amountB } = new Position({
id: this.id,
owner: this.owner,
pool: poolLower,
liquidity: positionThatWillBeCreated.liquidity,
tickLower: this.tickLower,
tickUpper: this.tickUpper,
feeGrowthInsideALastX64: this.feeGrowthInsideALastX64,
feeGrowthInsideBLastX64: this.feeGrowthInsideBLastX64,
feesA: this.feesA,
feesB: this.feesB,
}).mintAmounts;
return { amountA, amountB };
}
/**
* Returns the minimum amounts that should be requested in order to safely burn the amount of liquidity held by the
* position with the given slippage tolerance
* @param slippageTolerance tolerance of unfavorable slippage from the current price
* @returns The amounts, with slippage
*/
public burnAmountsWithSlippage(
slippageTolerance: Percent
): Readonly<{ amountA: CurrencyAmount<Token>; amountB: CurrencyAmount<Token> }> {
// get lower/upper prices
const { sqrtPriceX64Upper, sqrtPriceX64Lower } =
this.ratiosAfterSlippage(slippageTolerance);
// construct counterfactual pools
const poolLower = new Pool({
id: this.pool.id,
active: this.pool.active,
tokenA: this.pool.tokenA,
tokenB: this.pool.tokenB,
fee: this.pool.fee,
sqrtPriceX64: sqrtPriceX64Lower,
liquidity: 0 /* liquidity doesn't matter */,
tickCurrent: TickMath.getTickAtSqrtRatio(sqrtPriceX64Lower),
feeGrowthGlobalAX64: this.feeGrowthInsideALastX64,
feeGrowthGlobalBX64: this.feeGrowthInsideBLastX64,
ticks: this.pool.tickDataProvider
});
const poolUpper = new Pool({
id: this.pool.id,
active: this.pool.active,
tokenA: this.pool.tokenA,
tokenB: this.pool.tokenB,
fee: this.pool.fee,
sqrtPriceX64: sqrtPriceX64Upper,
liquidity: 0 /* liquidity doesn't matter */,
tickCurrent: TickMath.getTickAtSqrtRatio(sqrtPriceX64Upper),
feeGrowthGlobalAX64: this.feeGrowthInsideALastX64,
feeGrowthGlobalBX64: this.feeGrowthInsideBLastX64,
ticks: this.pool.tickDataProvider
});
// we want the smaller amounts...
// ...which occurs at the upper price for amountA...
const amountA = new Position({
id: this.id,
owner: this.owner,
pool: poolUpper,
liquidity: this.liquidity,
tickLower: this.tickLower,
tickUpper: this.tickUpper,
feeGrowthInsideALastX64: this.feeGrowthInsideALastX64,
feeGrowthInsideBLastX64: this.feeGrowthInsideBLastX64,
feesA: this.feesA,
feesB: this.feesB,
}).amountA;
// ...and the lower for amountB
const amountB = new Position({
id: this.id,
owner: this.owner,
pool: poolLower,
liquidity: this.liquidity,
tickLower: this.tickLower,
tickUpper: this.tickUpper,
feeGrowthInsideALastX64: this.feeGrowthInsideALastX64,
feeGrowthInsideBLastX64: this.feeGrowthInsideBLastX64,
feesA: this.feesA,
feesB: this.feesB,
}).amountB;
return { amountA: amountA, amountB: amountB };
}
/**
* Returns the minimum amounts that must be sent in order to mint the amount of liquidity held by the position at
* the current price for the pool
*/
public get mintAmounts(): Readonly<{ amountA: JSBI; amountB: JSBI }> {
if (this._mintAmounts === null) {
if (this.pool.tickCurrent < this.tickLower) {
return {
amountA: SqrtPriceMath.getAmountADelta(
TickMath.getSqrtRatioAtTick(this.tickLower),
TickMath.getSqrtRatioAtTick(this.tickUpper),
this.liquidity,
true
),
amountB: ZERO,
};
} else if (this.pool.tickCurrent < this.tickUpper) {
return {
amountA: SqrtPriceMath.getAmountADelta(
this.pool.sqrtPriceX64,
TickMath.getSqrtRatioAtTick(this.tickUpper),
this.liquidity,
true
),
amountB: SqrtPriceMath.getAmountBDelta(
TickMath.getSqrtRatioAtTick(this.tickLower),
this.pool.sqrtPriceX64,
this.liquidity,
true
),
};
} else {
return {
amountA: ZERO,
amountB: SqrtPriceMath.getAmountBDelta(
TickMath.getSqrtRatioAtTick(this.tickLower),
TickMath.getSqrtRatioAtTick(this.tickUpper),
this.liquidity,
true
),
};
}
}
return this._mintAmounts;
}
/**
* Computes the maximum amount of liquidity received for a given amount of tokenA, tokenB,
* and the prices at the tick boundaries.
* @param pool The pool for which the position should be created
* @param lower The lower tick of the position
* @param upper The upper tick of the position
* @param amountA tokenA amount
* @param amountB tokenB amount
* @param useFullPrecision If false, liquidity will be maximized according to what the router can calculate,
* not what core can theoretically support
* @returns The amount of liquidity for the position
*/
public static fromAmounts({
id,
owner,
pool,
tickLower,
tickUpper,
amountA,
amountB,
useFullPrecision,
feeGrowthInsideALastX64,
feeGrowthInsideBLastX64,
feesA,
feesB
}: {
id: number,
owner: string,
pool: Pool;
tickLower: number;
tickUpper: number;
amountA: BigintIsh;
amountB: BigintIsh;
useFullPrecision: boolean;
feeGrowthInsideALastX64: | BigintIsh,
feeGrowthInsideBLastX64: | BigintIsh,
feesA: BigintIsh,
feesB: BigintIsh
}) {
const sqrtRatioLX64 = TickMath.getSqrtRatioAtTick(tickLower);
const sqrtRatioUX64 = TickMath.getSqrtRatioAtTick(tickUpper);
return new Position({
id,
owner,
pool,
tickLower,
tickUpper,
liquidity: maxLiquidityForAmounts(
pool.sqrtPriceX64,
sqrtRatioLX64,
sqrtRatioUX64,
amountA,
amountB,
useFullPrecision
),
feeGrowthInsideALastX64,
feeGrowthInsideBLastX64,
feesA,
feesB
});
}
/**
* Computes a position with the maximum amount of liquidity received for a given amount of tokenA, assuming an unlimited amount of tokenB
* @param pool The pool for which the position is created
* @param lower The lower tick
* @param upper The upper tick
* @param amountA The desired amount of tokenA
* @param useFullPrecision If true, liquidity will be maximized according to what the router can calculate,
* not what core can theoretically support
* @returns The position
*/
public static fromAmountA({
id,
owner,
pool,
tickLower,
tickUpper,
amountA,
useFullPrecision,
feeGrowthInsideALastX64,
feeGrowthInsideBLastX64,
feesA,
feesB
}: {
id: number,
owner: string,
pool: Pool;
tickLower: number;
tickUpper: number;
amountA: BigintIsh;
useFullPrecision: boolean;
feeGrowthInsideALastX64: | BigintIsh;
feeGrowthInsideBLastX64: | BigintIsh;
feesA: | BigintIsh;
feesB: | BigintIsh;
}) {
return Position.fromAmounts({
id,
owner,
pool,
tickLower,
tickUpper,
amountA,
amountB: MaxUint64,
useFullPrecision,
feeGrowthInsideALastX64,
feeGrowthInsideBLastX64,
feesA,
feesB
});
}
/**
* Computes a position with the maximum amount of liquidity received for a given amount of tokenB, assuming an unlimited amount of tokenA
* @param pool The pool for which the position is created
* @param lower The lower tick
* @param upper The upper tick
* @param amountB The desired amount of tokenB
* @returns The position
*/
public static fromAmountB({
id,
owner,
pool,
tickLower,
tickUpper,
amountB,
feeGrowthInsideALastX64,
feeGrowthInsideBLastX64,
feesA,
feesB
}: {
id: number,
owner: string,
pool: Pool;
tickLower: number;
tickUpper: number;
amountB: BigintIsh;
feeGrowthInsideALastX64: | BigintIsh;
feeGrowthInsideBLastX64: | BigintIsh;
feesA: BigintIsh
feesB: BigintIsh
}) {
// this function always uses full precision,
return Position.fromAmounts({
id,
owner,
pool,
tickLower,
tickUpper,
amountA: MaxUint64,
amountB,
useFullPrecision: true,
feeGrowthInsideALastX64,
feeGrowthInsideBLastX64,
feesA,
feesB
});
}
/**
* Computes a position fees
* @returns The position
*/
public async getFees(): Promise<Fees> {
const { liquidity, tickLower, tickUpper, feeGrowthInsideALastX64, feeGrowthInsideBLastX64, pool } = this
if (
JSBI.equal(liquidity, ZERO) &&
JSBI.equal(this.feesA, ZERO) &&
JSBI.equal(this.feesB, ZERO)
) {
return {
feesA: CurrencyAmount.fromRawAmount(this.pool.tokenA, ZERO),
feesB: CurrencyAmount.fromRawAmount(this.pool.tokenB, ZERO)
}
}
const lower = this.pool.tickDataProvider.getTick(tickLower)
const upper = this.pool.tickDataProvider.getTick(tickUpper)
const { feeGrowthGlobalAX64, feeGrowthGlobalBX64 } = pool
const [feeGrowthInsideAX64, feeGrowthInsideBX64] = TickLibrary.getFeeGrowthInside(
lower,
upper,
tickLower,
tickUpper,
pool.tickCurrent,
feeGrowthGlobalAX64,
feeGrowthGlobalBX64
)
const tokensOwedA = JSBI.divide(
JSBI.multiply(
subIn128(feeGrowthInsideAX64, feeGrowthInsideALastX64),
liquidity
),
Q64
);
const tokensOwedB = JSBI.divide(
JSBI.multiply(
subIn128(feeGrowthInsideBX64, feeGrowthInsideBLastX64),
liquidity
),
Q64
);
return {
feesA: CurrencyAmount.fromRawAmount(this.pool.tokenA, JSBI.add(tokensOwedA, this.feesA)),
feesB: CurrencyAmount.fromRawAmount(this.pool.tokenB, JSBI.add(tokensOwedB, this.feesB))
}
}
}