import { BigintIsh, Price, sqrt, Token, CurrencyAmount, } from "@uniswap/sdk-core"; import invariant from "tiny-invariant"; import JSBI from "jsbi"; import { calculatePairAddressByHandler } from "../utils/pairs"; import { InsufficientInputAmountError, InsufficientReservesError, } from "./errors"; export const MINIMUM_LIQUIDITY = JSBI.BigInt(1000); // exports for internal consumption export const ZERO = JSBI.BigInt(0); export const ONE = JSBI.BigInt(1); export const FIVE = JSBI.BigInt(5); // eslint-disable-next-line @typescript-eslint/naming-convention export const _997 = JSBI.BigInt(997); // eslint-disable-next-line @typescript-eslint/naming-convention export const _1000 = JSBI.BigInt(1000); const calculatePairAddress = ( tokenA: Token, tokenB: Token, handler?: string ): string | undefined => { return calculatePairAddressByHandler(tokenA, tokenB, handler); }; export class Pair { public readonly liquidityToken: Token; public readonly handler?: string; // eslint-disable-next-line @typescript-eslint/naming-convention private readonly tokenAmounts: [CurrencyAmount, CurrencyAmount]; public static getAddress( tokenA: Token, tokenB: Token, handler?: string ): string { const address = calculatePairAddress(tokenA, tokenB, handler); if (!address) throw new Error("Pair: no address"); return address; } public constructor( currencyAmountA: CurrencyAmount, tokenAmountB: CurrencyAmount, handler?: string ) { const tokenAmounts = currencyAmountA.currency.sortsBefore( tokenAmountB.currency ) // does safety checks ? [currencyAmountA, tokenAmountB] : [tokenAmountB, currencyAmountA]; this.liquidityToken = new Token( tokenAmounts[0].currency.chainId, Pair.getAddress( tokenAmounts[0].currency, tokenAmounts[1].currency, handler ), 18, "UNI-V2", "Uniswap V2" ); this.handler = handler; this.tokenAmounts = tokenAmounts as [ CurrencyAmount, CurrencyAmount ]; } /** * Returns true if the token is either token0 or token1 * @param token to check */ public involvesToken(token: Token): boolean { return token.equals(this.token0) || token.equals(this.token1); } /** * Returns the current mid price of the pair in terms of token0, i.e. the ratio of reserve1 to reserve0 */ public get token0Price(): Price { const result = this.tokenAmounts[1].divide(this.tokenAmounts[0]); return new Price( this.token0, this.token1, result.denominator, result.numerator ); } /** * Returns the current mid price of the pair in terms of token1, i.e. the ratio of reserve0 to reserve1 */ public get token1Price(): Price { const result = this.tokenAmounts[0].divide(this.tokenAmounts[1]); return new Price( this.token1, this.token0, result.denominator, result.numerator ); } /** * Return the price of the given token in terms of the other token in the pair. * @param token token to return price of */ public priceOf(token: Token): Price { invariant(this.involvesToken(token), "TOKEN"); return token.equals(this.token0) ? this.token0Price : this.token1Price; } /** * Returns the chain ID of the tokens in the pair. */ public get chainId(): number { return this.token0.chainId; } public get token0(): Token { return this.tokenAmounts[0].currency; } public get token1(): Token { return this.tokenAmounts[1].currency; } public get reserve0(): CurrencyAmount { return this.tokenAmounts[0]; } public get reserve1(): CurrencyAmount { return this.tokenAmounts[1]; } public reserveOf(token: Token): CurrencyAmount { invariant(this.involvesToken(token), "TOKEN"); return token.equals(this.token0) ? this.reserve0 : this.reserve1; } public getOutputAmount( inputAmount: CurrencyAmount ): [CurrencyAmount, Pair] { invariant(this.involvesToken(inputAmount.currency), "TOKEN"); if ( JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO) ) { throw new InsufficientReservesError(); } const inputReserve = this.reserveOf(inputAmount.currency); const outputReserve = this.reserveOf( inputAmount.currency.equals(this.token0) ? this.token1 : this.token0 ); const inputAmountWithFee = JSBI.multiply(inputAmount.quotient, _997); const numerator = JSBI.multiply(inputAmountWithFee, outputReserve.quotient); const denominator = JSBI.add( JSBI.multiply(inputReserve.quotient, _1000), inputAmountWithFee ); const outputAmount = CurrencyAmount.fromRawAmount( inputAmount.currency.equals(this.token0) ? this.token1 : this.token0, JSBI.divide(numerator, denominator) ); if (JSBI.equal(outputAmount.quotient, ZERO)) { throw new InsufficientInputAmountError(); } return [ outputAmount, new Pair( inputReserve.add(inputAmount), outputReserve.subtract(outputAmount), this.handler ), ]; } public getInputAmount( outputAmount: CurrencyAmount ): [CurrencyAmount, Pair] { invariant(this.involvesToken(outputAmount.currency), "TOKEN"); if ( JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO) || JSBI.greaterThanOrEqual( outputAmount.quotient, this.reserveOf(outputAmount.currency).quotient ) ) { throw new InsufficientReservesError(); } const outputReserve = this.reserveOf(outputAmount.currency); const inputReserve = this.reserveOf( outputAmount.currency.equals(this.token0) ? this.token1 : this.token0 ); const numerator = JSBI.multiply( JSBI.multiply(inputReserve.quotient, outputAmount.quotient), _1000 ); const denominator = JSBI.multiply( JSBI.subtract(outputReserve.quotient, outputAmount.quotient), _997 ); const inputAmount = CurrencyAmount.fromRawAmount( outputAmount.currency.equals(this.token0) ? this.token1 : this.token0, JSBI.add(JSBI.divide(numerator, denominator), ONE) ); return [ inputAmount, new Pair( inputReserve.add(inputAmount), outputReserve.subtract(outputAmount), this.handler ), ]; } public getLiquidityMinted( totalSupply: CurrencyAmount, tokenAmountA: CurrencyAmount, tokenAmountB: CurrencyAmount ): CurrencyAmount { invariant(totalSupply.currency.equals(this.liquidityToken), "LIQUIDITY"); const tokenAmounts = tokenAmountA.currency.sortsBefore( tokenAmountB.currency ) // does safety checks ? [tokenAmountA, tokenAmountB] : [tokenAmountB, tokenAmountA]; invariant( tokenAmounts[0].currency.equals(this.token0) && tokenAmounts[1].currency.equals(this.token1), "TOKEN" ); let liquidity: JSBI; if (JSBI.equal(totalSupply.quotient, ZERO)) { liquidity = JSBI.subtract( sqrt(JSBI.multiply(tokenAmounts[0].quotient, tokenAmounts[1].quotient)), MINIMUM_LIQUIDITY ); } else { const amount0 = JSBI.divide( JSBI.multiply(tokenAmounts[0].quotient, totalSupply.quotient), this.reserve0.quotient ); const amount1 = JSBI.divide( JSBI.multiply(tokenAmounts[1].quotient, totalSupply.quotient), this.reserve1.quotient ); liquidity = JSBI.lessThanOrEqual(amount0, amount1) ? amount0 : amount1; } if (!JSBI.greaterThan(liquidity, ZERO)) { throw new Error("Pair: InsufficientInputAmountError"); } return CurrencyAmount.fromRawAmount(this.liquidityToken, liquidity); } public getLiquidityValue( token: Token, totalSupply: CurrencyAmount, liquidity: CurrencyAmount, feeOn = false, kLast?: BigintIsh ): CurrencyAmount { invariant(this.involvesToken(token), "TOKEN"); invariant(totalSupply.currency.equals(this.liquidityToken), "TOTAL_SUPPLY"); invariant(liquidity.currency.equals(this.liquidityToken), "LIQUIDITY"); invariant( JSBI.lessThanOrEqual(liquidity.quotient, totalSupply.quotient), "LIQUIDITY" ); let totalSupplyAdjusted: CurrencyAmount; if (!feeOn) { totalSupplyAdjusted = totalSupply; } else { invariant(!!kLast, "K_LAST"); const kLastParsed = JSBI.BigInt(kLast); if (!JSBI.equal(kLastParsed, ZERO)) { const rootK = sqrt( JSBI.multiply(this.reserve0.quotient, this.reserve1.quotient) ); const rootKLast = sqrt(kLastParsed); if (JSBI.greaterThan(rootK, rootKLast)) { const numerator = JSBI.multiply( totalSupply.quotient, JSBI.subtract(rootK, rootKLast) ); const denominator = JSBI.add(JSBI.multiply(rootK, FIVE), rootKLast); const feeLiquidity = JSBI.divide(numerator, denominator); totalSupplyAdjusted = totalSupply.add( CurrencyAmount.fromRawAmount(this.liquidityToken, feeLiquidity) ); } else { totalSupplyAdjusted = totalSupply; } } else { totalSupplyAdjusted = totalSupply; } } return CurrencyAmount.fromRawAmount( token, JSBI.divide( JSBI.multiply(liquidity.quotient, this.reserveOf(token).quotient), totalSupplyAdjusted.quotient ) ); } }