@alcorexchange/alcor-swap-sdk
Version:
## Installation **npm** ``` npm i @alcorexchange/alcor-swap-sdk ``` **yarn** ``` yarn add @alcorexchange/alcor-swap-sdk ``` ## Usage ### Import:
627 lines (558 loc) • 22.7 kB
text/typescript
import _ from 'lodash'
import invariant from 'tiny-invariant'
import { Currency } from './currency'
import { Fraction, Percent, Price, CurrencyAmount } from './fractions'
import { sortedInsert } from '../utils'
import { Token } from './token'
import { ONE, ZERO, TradeType } from '../internalConstants'
import { Route } from './route'
import { getBestSwapRoute } from '../utils/getBestSwapRoute'
/**
* Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The trade type, either exact input or exact output
* @param a The first trade to compare
* @param b The second trade to compare
* @returns A sorted ordering for two neighboring elements in a trade array
*/
export function tradeComparator<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
a: Trade<TInput, TOutput, TTradeType>,
b: Trade<TInput, TOutput, TTradeType>
) {
// must have same input and output token for comparison
invariant(a.inputAmount.currency.equals(b.inputAmount.currency), 'INPUT_CURRENCY')
invariant(a.outputAmount.currency.equals(b.outputAmount.currency), 'OUTPUT_CURRENCY')
if (a.outputAmount.equalTo(b.outputAmount)) {
if (a.inputAmount.equalTo(b.inputAmount)) {
// consider the number of hops since each hop costs cpu
const aHops = a.swaps.reduce((total, cur) => total + cur.route.tokenPath.length, 0)
const bHops = b.swaps.reduce((total, cur) => total + cur.route.tokenPath.length, 0)
return aHops - bHops
}
// 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
}
}
}
export interface BestTradeOptions {
// how many results to return
maxNumResults?: number
// the maximum number of hops a trade should contain
maxHops?: number
}
/**
* Represents a trade executed against a set of routes where some percentage of the input is
* split across each route.
*
* Each route has its own set of pools. Pools can not be re-used across routes.
*
* Does not account for slippage, i.e., changes in price environment that can occur between
* the time the trade is submitted and when it is executed.
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The trade type, either exact input or exact output
*/
export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType> {
/**
* @deprecated Deprecated in favor of 'swaps' property. If the trade consists of multiple routes
* this will return an error.
*
* When the trade consists of just a single route, this returns the route of the trade,
* i.e. which pools the trade goes through.
*/
public get route(): Route<TInput, TOutput> {
invariant(this.swaps.length == 1, 'MULTIPLE_ROUTES')
return this.swaps[0].route
}
/**
* The swaps of the trade, i.e. which routes and how much is swapped in each that
* make up the trade.
*/
public readonly swaps: {
percent: number,
route: Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
/**
* The type of the trade, either exact in or exact out.
*/
public readonly tradeType: TTradeType
/**
* The cached result of the input amount computation
* @private
*/
private _inputAmount: CurrencyAmount<TInput> | undefined
/**
* The input amount for the trade assuming no slippage.
*/
public get inputAmount(): CurrencyAmount<TInput> {
if (this._inputAmount) {
return this._inputAmount
}
const inputCurrency = this.swaps[0].inputAmount.currency
const totalInputFromRoutes = this.swaps
.map(({ inputAmount }) => inputAmount)
.reduce((total, cur) => total.add(cur), CurrencyAmount.fromRawAmount(inputCurrency, 0))
this._inputAmount = totalInputFromRoutes
return this._inputAmount
}
/**
* The cached result of the output amount computation
* @private
*/
private _outputAmount: CurrencyAmount<TOutput> | undefined
/**
* The output amount for the trade assuming no slippage.
*/
public get outputAmount(): CurrencyAmount<TOutput> {
if (this._outputAmount) {
return this._outputAmount
}
const outputCurrency = this.swaps[0].outputAmount.currency
const totalOutputFromRoutes = this.swaps
.map(({ outputAmount }) => outputAmount)
.reduce((total, cur) => total.add(cur), CurrencyAmount.fromRawAmount(outputCurrency, 0))
this._outputAmount = totalOutputFromRoutes
return this._outputAmount
}
/**
* The cached result of the computed execution price
* @private
*/
private _executionPrice: Price<TInput, TOutput> | undefined
/**
* The price expressed in terms of output amount/input amount.
*/
public get executionPrice(): Price<TInput, TOutput> {
return (
this._executionPrice ??
(this._executionPrice = new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.inputAmount.quotient,
this.outputAmount.quotient
))
)
}
/**
* The cached result of the price impact computation
* @private
*/
private _priceImpact: Percent | undefined
/**
* Returns the percent difference between the route's mid price and the price impact
*/
public get priceImpact(): Percent {
if (this._priceImpact) {
return this._priceImpact
}
let spotOutputAmount = CurrencyAmount.fromRawAmount(this.outputAmount.currency, 0)
for (const { route, inputAmount } of this.swaps) {
const midPrice = route.midPrice
spotOutputAmount = spotOutputAmount.add(midPrice.quote(inputAmount))
}
const priceImpact = spotOutputAmount.subtract(this.outputAmount).divide(spotOutputAmount)
this._priceImpact = new Percent(priceImpact.numerator, priceImpact.denominator)
return this._priceImpact
}
/**
* Constructs an exact in trade with the given amount in and route
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @param route The route of the exact in trade
* @param amountIn The amount being passed in
* @returns The exact in trade
*/
public static exactIn<TInput extends Currency, TOutput extends Currency>(
route: Route<TInput, TOutput>,
amountIn: CurrencyAmount<TInput>
): Trade<TInput, TOutput, TradeType.EXACT_INPUT> {
return Trade.fromRoute(route, amountIn, TradeType.EXACT_INPUT)
}
/**
* Constructs an exact out trade with the given amount out and route
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @param route The route of the exact out trade
* @param amountOut The amount returned by the trade
* @returns The exact out trade
*/
public static exactOut<TInput extends Currency, TOutput extends Currency>(
route: Route<TInput, TOutput>,
amountOut: CurrencyAmount<TOutput>
): Trade<TInput, TOutput, TradeType.EXACT_OUTPUT> {
return Trade.fromRoute(route, amountOut, TradeType.EXACT_OUTPUT)
}
/**
* Constructs a trade by simulating swaps through the given route
* @template TInput The input token, either Ether or an ERC-20.
* @template TOutput The output token, either Ether or an ERC-20.
* @template TTradeType The type of the trade, either exact in or exact out.
* @param route route to swap through
* @param amount the amount specified, either input or output, depending on tradeType
* @param tradeType whether the trade is an exact input or exact output swap
* @returns The route
*/
public static fromRoute<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
route: Route<TInput, TOutput>,
amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>,
tradeType: TTradeType,
percent = 100
): Trade<TInput, TOutput, TTradeType> {
const amounts: CurrencyAmount<Token>[] = new Array(route.tokenPath.length);
let inputAmount: CurrencyAmount<any>;
let outputAmount: CurrencyAmount<any>;
if (tradeType === TradeType.EXACT_INPUT) {
amounts[0] = amount; // Переиспользуем amount напрямую
for (let i = 0; i < route.tokenPath.length - 1; i++) {
amounts[i + 1] = route.pools[i].getOutputAmount(amounts[i]);
}
inputAmount = amount; // Без создания нового объекта
outputAmount = amounts[amounts.length - 1];
} else {
amounts[amounts.length - 1] = amount;
for (let i = route.tokenPath.length - 1; i > 0; i--) {
amounts[i - 1] = route.pools[i - 1].getInputAmount(amounts[i]);
}
inputAmount = amounts[0];
outputAmount = amount;
}
return new Trade({
routes: [{ inputAmount, outputAmount, route, percent }],
tradeType
});
}
/**
* Constructs a trade from routes by simulating swaps
*
* @template TInput The input token, either Ether or an ERC-20.
* @template TOutput The output token, either Ether or an ERC-20.
* @template TTradeType The type of the trade, either exact in or exact out.
* @param routes the routes to swap through and how much of the amount should be routed through each
* @param tradeType whether the trade is an exact input or exact output swap
* @returns The trade
*/
public static fromRoutes<TInput extends Currency, TOutput extends Currency, TTradeType extends TradeType>(
routes: {
amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount<TInput> : CurrencyAmount<TOutput>
route: Route<TInput, TOutput>,
percent: number
}[],
tradeType: TTradeType
): Trade<TInput, TOutput, TTradeType> {
const populatedRoutes: {
percent: number,
route: Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[] = []
for (const { route, amount, percent } of routes) {
const amounts: CurrencyAmount<Token>[] = new Array(route.tokenPath.length)
let inputAmount: CurrencyAmount<TInput>
let outputAmount: CurrencyAmount<TOutput>
if (tradeType === TradeType.EXACT_INPUT) {
invariant(amount.currency.equals(route.input), 'INPUT')
inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator)
amounts[0] = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator)
for (let i = 0; i < route.tokenPath.length - 1; i++) {
const pool = route.pools[i]
const outputAmount = pool.getOutputAmount(amounts[i])
amounts[i + 1] = outputAmount
}
outputAmount = CurrencyAmount.fromFractionalAmount(
route.output,
amounts[amounts.length - 1].numerator,
amounts[amounts.length - 1].denominator
)
} else {
invariant(amount.currency.equals(route.output), 'OUTPUT')
outputAmount = CurrencyAmount.fromFractionalAmount(route.output, amount.numerator, amount.denominator)
amounts[amounts.length - 1] = CurrencyAmount.fromFractionalAmount(
route.output,
amount.numerator,
amount.denominator
)
for (let i = route.tokenPath.length - 1; i > 0; i--) {
const pool = route.pools[i - 1]
const inputAmount = pool.getInputAmount(amounts[i])
amounts[i - 1] = inputAmount
}
inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amounts[0].numerator, amounts[0].denominator)
}
populatedRoutes.push({ route, inputAmount, outputAmount, percent })
}
return new Trade({
routes: populatedRoutes,
tradeType
})
}
/**
* Creates a trade without computing the result of swapping through the route. Useful when you have simulated the trade
* elsewhere and do not have any tick data
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The type of the trade, either exact in or exact out
* @param constructorArguments The arguments passed to the trade constructor
* @returns The unchecked trade
*/
public static createUncheckedTrade<
TInput extends Currency,
TOutput extends Currency,
TTradeType extends TradeType
>(constructorArguments: {
percent: number,
route: Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
tradeType: TTradeType
}): Trade<TInput, TOutput, TTradeType> {
return new Trade({
...constructorArguments,
routes: [
{
percent: constructorArguments.percent,
inputAmount: constructorArguments.inputAmount,
outputAmount: constructorArguments.outputAmount,
route: constructorArguments.route
}
]
})
}
/**
* Creates a trade without computing the result of swapping through the routes. Useful when you have simulated the trade
* elsewhere and do not have any tick data
* @template TInput The input token, either Ether or an ERC-20
* @template TOutput The output token, either Ether or an ERC-20
* @template TTradeType The type of the trade, either exact in or exact out
* @param constructorArguments The arguments passed to the trade constructor
* @returns The unchecked trade
*/
public static createUncheckedTradeWithMultipleRoutes<
TInput extends Currency,
TOutput extends Currency,
TTradeType extends TradeType
>(constructorArguments: {
routes: {
percent: number
route: Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
tradeType: TTradeType
}): Trade<TInput, TOutput, TTradeType> {
return new Trade(constructorArguments)
}
/**
* Construct a trade by passing in the pre-computed property values
* @param routes The routes through which the trade occurs
* @param tradeType The type of trade, exact input or exact output
*/
private constructor({
routes,
tradeType
}: {
routes: {
percent: number,
route: Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
tradeType: TTradeType
}) {
const inputCurrency = routes[0].inputAmount.currency
const outputCurrency = routes[0].outputAmount.currency
invariant(
routes.every(({ route }) => inputCurrency.equals(route.input)),
'INPUT_CURRENCY_MATCH'
)
invariant(
routes.every(({ route }) => outputCurrency.equals(route.output)),
'OUTPUT_CURRENCY_MATCH'
)
const numPools = routes.map(({ route }) => route.pools.length).reduce((total, cur) => total + cur, 0)
const poolAddressSet = new Set<number>()
for (const { route } of routes) {
for (const pool of route.pools) {
poolAddressSet.add(pool.id)
}
}
invariant(numPools == poolAddressSet.size, 'POOLS_DUPLICATED')
this.swaps = routes
this.tradeType = tradeType
}
/**
* Get the minimum amount that must be received from this trade for the given slippage tolerance
* @param slippageTolerance The tolerance of unfavorable slippage from the execution price of this trade
* @returns The amount out
*/
public minimumAmountOut(slippageTolerance: Percent, amountOut = this.outputAmount): CurrencyAmount<TOutput> {
invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
if (this.tradeType === TradeType.EXACT_OUTPUT) {
return amountOut
} else {
const slippageAdjustedAmountOut = new Fraction(ONE)
.add(slippageTolerance)
.invert()
.multiply(amountOut.quotient).quotient
return CurrencyAmount.fromRawAmount(amountOut.currency, slippageAdjustedAmountOut)
}
}
/**
* Get the maximum amount in that can be spent via this trade for the given slippage tolerance
* @param slippageTolerance The tolerance of unfavorable slippage from the execution price of this trade
* @returns The amount in
*/
public maximumAmountIn(slippageTolerance: Percent, amountIn = this.inputAmount): CurrencyAmount<TInput> {
invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE')
if (this.tradeType === TradeType.EXACT_INPUT) {
return amountIn
} else {
const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(amountIn.quotient).quotient
return CurrencyAmount.fromRawAmount(amountIn.currency, slippageAdjustedAmountIn)
}
}
/**
* Return the execution price after accounting for slippage tolerance
* @param slippageTolerance the allowed tolerated slippage
* @returns The execution price
*/
public worstExecutionPrice(slippageTolerance: Percent): Price<TInput, TOutput> {
return new Price(
this.inputAmount.currency,
this.outputAmount.currency,
this.maximumAmountIn(slippageTolerance).quotient,
this.minimumAmountOut(slippageTolerance).quotient
)
}
public static bestTradeExactIn<TInput extends Currency, TOutput extends Currency>(
routes: Route<TInput, TOutput>[],
currencyAmountIn: CurrencyAmount<TInput>,
maxNumResults = 1,
): Trade<TInput, TOutput, TradeType.EXACT_INPUT>[] {
invariant(routes.length > 0, 'ROUTES')
const bestTrades: Trade<TInput, TOutput, TradeType.EXACT_INPUT>[] = []
for (const route of routes) {
let trade
try {
trade = Trade.fromRoute(route, currencyAmountIn, TradeType.EXACT_INPUT)
} catch (error) {
// not enough liquidity in this pair
if ((error as any).isInsufficientInputAmountError) {
continue
}
throw error
}
// FIXME! Sorting bug multiple pools
if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) continue
sortedInsert(
bestTrades,
trade,
maxNumResults,
tradeComparator
)
}
return bestTrades
}
public static bestTradeExactOut<TInput extends Currency, TOutput extends Currency>(
routes: Route<TInput, TOutput>[],
currencyAmountOut: CurrencyAmount<TOutput>,
maxNumResults = 1,
): Trade<TInput, TOutput, TradeType.EXACT_OUTPUT>[] {
invariant(routes.length > 0, 'ROUTES')
const bestTrades: Trade<TInput, TOutput, TradeType.EXACT_OUTPUT>[] = []
for (const route of routes) {
let trade
try {
trade = Trade.fromRoute(route, currencyAmountOut, TradeType.EXACT_OUTPUT)
} catch (error) {
// not enough liquidity in this pair
if ((error as any).isInsufficientReservesError) {
continue
}
throw error
}
if (!trade.inputAmount.greaterThan(0) || !trade.priceImpact.greaterThan(0)) continue
sortedInsert(
bestTrades,
trade,
maxNumResults,
tradeComparator
)
}
return bestTrades
}
public static bestTradeWithSplit<TInput extends Currency, TOutput extends Currency>(
_routes: Route<TInput, TOutput>[],
amount: CurrencyAmount<Currency>,
percents: number[],
tradeType: TradeType,
swapConfig = { minSplits: 1, maxSplits: 10 }
): Trade<Currency, Currency, TradeType> | null {
invariant(_routes.length > 0, 'ROUTES')
invariant(percents.length > 0, 'PERCENTS')
// Предварительно вычисляем splitAmount для всех процентов
const percentToAmount = new Map<number, CurrencyAmount<Currency>>();
for (const percent of percents) {
percentToAmount.set(percent, amount.multiply(percent).divide(100));
}
// Используем Map вместо объекта для лучшей производительности
const percentToTrades = new Map<number, Trade<Currency, Currency, TradeType>[]>();
for (const percent of percents) {
percentToTrades.set(percent, []);
}
// Оптимизируем внутренний цикл - группируем вычисления по маршрутам
for (const route of _routes) {
// Для каждого маршрута проходим по всем процентам
for (const percent of percents) {
const splitAmount = percentToAmount.get(percent)!;
try {
const trade = Trade.fromRoute(route, splitAmount, tradeType, percent);
if (trade.inputAmount.greaterThan(0) && trade.priceImpact.greaterThan(0)) {
percentToTrades.get(percent)!.push(trade);
}
} catch (error) {
if ((error as any).isInsufficientReservesError || (error as any).isInsufficientInputAmountError) {
continue;
}
throw error;
}
}
}
// Преобразуем Map обратно в объект для совместимости с getBestSwapRoute
const percentToTradesObj: { [percent: number]: Trade<Currency, Currency, TradeType>[] } = {};
percentToTrades.forEach((trades, percent) => {
percentToTradesObj[percent] = trades;
});
const bestTrades = getBestSwapRoute(tradeType, percentToTradesObj, percents, swapConfig);
if (!bestTrades) return null
const routes = bestTrades.map(({ inputAmount, outputAmount, route, swaps }) => {
return { inputAmount, outputAmount, route, percent: swaps[0].percent }
})
// Check missing input after splitting
// TODO Do we need it for exact out?
if (tradeType === TradeType.EXACT_INPUT) {
const totalAmount = _.reduce(routes, (total, route) =>
total.add(route.inputAmount),
CurrencyAmount.fromRawAmount(routes[0].route.input, 0)
)
const missingAmount = amount.subtract(totalAmount)
if (missingAmount.greaterThan(0)) {
console.log("MISSING AMOUNT!!!", missingAmount.toFixed())
routes[0].inputAmount = routes[0].inputAmount.add(missingAmount)
}
}
return new Trade({ routes, tradeType })
}
}