UNPKG

@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
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 }) } }