@evolutionland/evolution-js
Version:
evolution evolution-js evolutionland evolution-js-sdk evolution-land metaverse
308 lines (307 loc) • 15 kB
JavaScript
import invariant from "tiny-invariant";
import { ONE, TradeType, ZERO } from "../constants";
import { sortedInsert } from "../utils";
import { ETHER } from "./currency";
import { CurrencyAmount } from "./fractions/currencyAmount";
import { Fraction } from "./fractions/fraction";
import { Percent } from "./fractions/percent";
import { Price } from "./fractions/price";
import { TokenAmount } from "./fractions/tokenAmount";
import { Route } from "./route";
import { currencyEquals, Token, WETH } from "./token";
/**
* Returns the percent difference between the mid price and the execution price, i.e. price impact.
* @param midPrice mid price before the trade
* @param inputAmount the input amount of the trade
* @param outputAmount the output amount of the trade
*/
function computePriceImpact(midPrice, inputAmount, outputAmount) {
const exactQuote = midPrice.raw.multiply(inputAmount.raw);
// calculate slippage := (exactQuote - outputAmount) / exactQuote
const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote);
return new Percent(slippage.numerator, slippage.denominator);
}
// comparator function that allows sorting trades by their output amounts, in decreasing order, and then input amounts
// in increasing order. i.e. the best trades have the most outputs for the least inputs and are sorted first
export function inputOutputComparator(a, b) {
// must have same input and output token for comparison
invariant(currencyEquals(a.inputAmount.currency, b.inputAmount.currency), "INPUT_CURRENCY");
invariant(currencyEquals(a.outputAmount.currency, b.outputAmount.currency), "OUTPUT_CURRENCY");
if (a.outputAmount.equalTo(b.outputAmount)) {
if (a.inputAmount.equalTo(b.inputAmount)) {
return 0;
}
// trade A requires less input than trade B, so A should come first
if (a.inputAmount.lessThan(b.inputAmount)) {
return -1;
}
else {
return 1;
}
}
else {
// tradeA has less output than trade B, so should come second
if (a.outputAmount.lessThan(b.outputAmount)) {
return 1;
}
else {
return -1;
}
}
}
// extension of the input output comparator that also considers other dimensions of the trade in ranking them
export function tradeComparator(a, b) {
const ioComp = inputOutputComparator(a, b);
if (ioComp !== 0) {
return ioComp;
}
// consider lowest slippage next, since these are less likely to fail
if (a.priceImpact.lessThan(b.priceImpact)) {
return -1;
}
else if (a.priceImpact.greaterThan(b.priceImpact)) {
return 1;
}
// finally consider the number of hops since each hop costs gas
return a.route.path.length - b.route.path.length;
}
/**
* Given a currency amount and a chain ID, returns the equivalent representation as the token amount.
* In other words, if the currency is ETHER, returns the WETH token amount for the given chain. Otherwise, returns
* the input currency amount.
*/
function wrappedAmount(currencyAmount, chainId) {
if (currencyAmount instanceof TokenAmount)
return currencyAmount;
if (currencyAmount.currency === ETHER)
return new TokenAmount(WETH[chainId], currencyAmount.raw);
invariant(false, "CURRENCY");
}
function wrappedCurrency(currency, chainId) {
if (currency instanceof Token)
return currency;
if (currency === ETHER)
return WETH[chainId];
invariant(false, "CURRENCY");
}
/**
* Represents a trade executed against a list of pairs.
* Does not account for slippage, i.e. trades that front run this trade and move the price.
*/
export class Trade {
constructor(route, amount, tradeType) {
const amounts = new Array(route.path.length);
const nextPairs = new Array(route.pairs.length);
if (tradeType === TradeType.EXACT_INPUT) {
invariant(currencyEquals(amount.currency, route.input), "INPUT");
amounts[0] = wrappedAmount(amount, route.chainId);
for (let i = 0; i < route.path.length - 1; i++) {
const pair = route.pairs[i];
const [outputAmount, nextPair] = pair.getOutputAmount(amounts[i]);
amounts[i + 1] = outputAmount;
nextPairs[i] = nextPair;
}
}
else {
invariant(currencyEquals(amount.currency, route.output), "OUTPUT");
amounts[amounts.length - 1] = wrappedAmount(amount, route.chainId);
for (let i = route.path.length - 1; i > 0; i--) {
const pair = route.pairs[i - 1];
const [inputAmount, nextPair] = pair.getInputAmount(amounts[i]);
amounts[i - 1] = inputAmount;
nextPairs[i - 1] = nextPair;
}
}
this.route = route;
this.tradeType = tradeType;
this.inputAmount =
tradeType === TradeType.EXACT_INPUT
? amount
: route.input === ETHER
? CurrencyAmount.ether(amounts[0].raw)
: amounts[0];
this.outputAmount =
tradeType === TradeType.EXACT_OUTPUT
? amount
: route.output === ETHER
? CurrencyAmount.ether(amounts[amounts.length - 1].raw)
: amounts[amounts.length - 1];
this.executionPrice = new Price(this.inputAmount.currency, this.outputAmount.currency, this.inputAmount.raw, this.outputAmount.raw);
this.nextMidPrice = Price.fromRoute(new Route(nextPairs, route.input));
this.priceImpact = computePriceImpact(route.midPrice, this.inputAmount, this.outputAmount);
}
/**
* Constructs an exact in trade with the given amount in and route
* @param route route of the exact in trade
* @param amountIn the amount being passed in
*/
static exactIn(route, amountIn) {
return new Trade(route, amountIn, TradeType.EXACT_INPUT);
}
/**
* Constructs an exact out trade with the given amount out and route
* @param route route of the exact out trade
* @param amountOut the amount returned by the trade
*/
static exactOut(route, amountOut) {
return new Trade(route, amountOut, TradeType.EXACT_OUTPUT);
}
/**
* Get the minimum amount that must be received from this trade for the given slippage tolerance
* @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade
*/
minimumAmountOut(slippageTolerance) {
invariant(!slippageTolerance.lessThan(ZERO), "SLIPPAGE_TOLERANCE");
if (this.tradeType === TradeType.EXACT_OUTPUT) {
return this.outputAmount;
}
else {
const slippageAdjustedAmountOut = new Fraction(ONE)
.add(slippageTolerance)
.invert()
.multiply(this.outputAmount.raw).quotient;
return this.outputAmount instanceof TokenAmount
? new TokenAmount(this.outputAmount.token, slippageAdjustedAmountOut)
: CurrencyAmount.ether(slippageAdjustedAmountOut);
}
}
/**
* Get the maximum amount in that can be spent via this trade for the given slippage tolerance
* @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade
*/
maximumAmountIn(slippageTolerance) {
invariant(!slippageTolerance.lessThan(ZERO), "SLIPPAGE_TOLERANCE");
if (this.tradeType === TradeType.EXACT_INPUT) {
return this.inputAmount;
}
else {
const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient;
return this.inputAmount instanceof TokenAmount
? new TokenAmount(this.inputAmount.token, slippageAdjustedAmountIn)
: CurrencyAmount.ether(slippageAdjustedAmountIn);
}
}
/**
* Given a list of pairs, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token
* amount to an output token, making at most `maxHops` hops.
* Note this does not consider aggregation, as routes are linear. It's possible a better route exists by splitting
* the amount in among multiple routes.
* @param pairs the pairs to consider in finding the best trade
* @param currencyAmountIn exact amount of input currency to spend
* @param currencyOut the desired currency out
* @param maxNumResults maximum number of results to return
* @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair
* @param currentPairs used in recursion; the current list of pairs
* @param originalAmountIn used in recursion; the original value of the currencyAmountIn parameter
* @param bestTrades used in recursion; the current list of best trades
*/
static bestTradeExactIn(pairs, currencyAmountIn, currencyOut, { maxNumResults = 3, maxHops = 3 } = {},
// used in recursion.
currentPairs = [], originalAmountIn = currencyAmountIn, bestTrades = []) {
invariant(pairs.length > 0, "PAIRS");
invariant(maxHops > 0, "MAX_HOPS");
invariant(originalAmountIn === currencyAmountIn || currentPairs.length > 0, "INVALID_RECURSION");
const chainId = currencyAmountIn instanceof TokenAmount
? currencyAmountIn.token.chainId
: currencyOut instanceof Token
? currencyOut.chainId
: undefined;
invariant(chainId !== undefined, "CHAIN_ID");
const amountIn = wrappedAmount(currencyAmountIn, chainId);
const tokenOut = wrappedCurrency(currencyOut, chainId);
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
// pair irrelevant
if (!pair.token0.equals(amountIn.token) && !pair.token1.equals(amountIn.token))
continue;
if (pair.reserve0.equalTo(ZERO) || pair.reserve1.equalTo(ZERO))
continue;
let amountOut;
try {
[amountOut] = pair.getOutputAmount(amountIn);
}
catch (error) {
// input too low
if (error.isInsufficientInputAmountError) {
continue;
}
throw error;
}
// we have arrived at the output token, so this is the final trade of one of the paths
if (amountOut.token.equals(tokenOut)) {
sortedInsert(bestTrades, new Trade(new Route([...currentPairs, pair], originalAmountIn.currency, currencyOut), originalAmountIn, TradeType.EXACT_INPUT), maxNumResults, tradeComparator);
}
else if (maxHops > 1 && pairs.length > 1) {
const pairsExcludingThisPair = pairs.slice(0, i).concat(pairs.slice(i + 1, pairs.length));
// otherwise, consider all the other paths that lead from this token as long as we have not exceeded maxHops
Trade.bestTradeExactIn(pairsExcludingThisPair, amountOut, currencyOut, {
maxNumResults,
maxHops: maxHops - 1,
}, [...currentPairs, pair], originalAmountIn, bestTrades);
}
}
return bestTrades;
}
/**
* similar to the above method but instead targets a fixed output amount
* given a list of pairs, and a fixed amount out, returns the top `maxNumResults` trades that go from an input token
* to an output token amount, making at most `maxHops` hops
* note this does not consider aggregation, as routes are linear. it's possible a better route exists by splitting
* the amount in among multiple routes.
* @param pairs the pairs to consider in finding the best trade
* @param currencyIn the currency to spend
* @param currencyAmountOut the exact amount of currency out
* @param maxNumResults maximum number of results to return
* @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair
* @param currentPairs used in recursion; the current list of pairs
* @param originalAmountOut used in recursion; the original value of the currencyAmountOut parameter
* @param bestTrades used in recursion; the current list of best trades
*/
static bestTradeExactOut(pairs, currencyIn, currencyAmountOut, { maxNumResults = 3, maxHops = 3 } = {},
// used in recursion.
currentPairs = [], originalAmountOut = currencyAmountOut, bestTrades = []) {
invariant(pairs.length > 0, "PAIRS");
invariant(maxHops > 0, "MAX_HOPS");
invariant(originalAmountOut === currencyAmountOut || currentPairs.length > 0, "INVALID_RECURSION");
const chainId = currencyAmountOut instanceof TokenAmount
? currencyAmountOut.token.chainId
: currencyIn instanceof Token
? currencyIn.chainId
: undefined;
invariant(chainId !== undefined, "CHAIN_ID");
const amountOut = wrappedAmount(currencyAmountOut, chainId);
const tokenIn = wrappedCurrency(currencyIn, chainId);
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
// pair irrelevant
if (!pair.token0.equals(amountOut.token) && !pair.token1.equals(amountOut.token))
continue;
if (pair.reserve0.equalTo(ZERO) || pair.reserve1.equalTo(ZERO))
continue;
let amountIn;
try {
[amountIn] = pair.getInputAmount(amountOut);
}
catch (error) {
// not enough liquidity in this pair
if (error.isInsufficientReservesError) {
continue;
}
throw error;
}
// we have arrived at the input token, so this is the first trade of one of the paths
if (amountIn.token.equals(tokenIn)) {
sortedInsert(bestTrades, new Trade(new Route([pair, ...currentPairs], currencyIn, originalAmountOut.currency), originalAmountOut, TradeType.EXACT_OUTPUT), maxNumResults, tradeComparator);
}
else if (maxHops > 1 && pairs.length > 1) {
const pairsExcludingThisPair = pairs.slice(0, i).concat(pairs.slice(i + 1, pairs.length));
// otherwise, consider all the other paths that arrive at this token as long as we have not exceeded maxHops
Trade.bestTradeExactOut(pairsExcludingThisPair, currencyIn, amountIn, {
maxNumResults,
maxHops: maxHops - 1,
}, [pair, ...currentPairs], originalAmountOut, bestTrades);
}
}
return bestTrades;
}
}