import { Currency, Fraction, Percent, Price, sortedInsert, CurrencyAmount, TradeType, Token } from '@uniswap/sdk-core' import JSBI from 'jsbi' import invariant from 'tiny-invariant' import { ONE, ZERO } from '../internalConstants' import { Pool } from './pool' import { Route } from './route' import { amountWithPathCurrency } from '../utils/pathCurrency' /** * Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them * @template TInput The input currency, either Ether or an ERC-20 * @template TOutput The output currency, 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( a: Trade, b: Trade ) { // must have same input and output currency 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 gas const aHops = a.swaps.reduce((total, cur) => total + cur.route.currencyPath.length, 0) const bHops = b.swaps.reduce((total, cur) => total + cur.route.currencyPath.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 currency, either Ether or an ERC-20 * @template TOutput The output currency, either Ether or an ERC-20 * @template TTradeType The trade type, either exact input or exact output */ export class Trade { /** * @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 { 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: { route: Route inputAmount: CurrencyAmount outputAmount: CurrencyAmount }[] /** * 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 | undefined /** * The input amount for the trade assuming no slippage. */ public get inputAmount(): CurrencyAmount { 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 | undefined /** * The output amount for the trade assuming no slippage. */ public get outputAmount(): CurrencyAmount { 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 | undefined /** * The price expressed in terms of output amount/input amount. */ public get executionPrice(): Price { 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 currency, either Ether or an ERC-20 * @template TOutput The output currency, 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 async exactIn( route: Route, amountIn: CurrencyAmount ): Promise> { return Trade.fromRoute(route, amountIn, TradeType.EXACT_INPUT) } /** * Constructs an exact out trade with the given amount out and route * @template TInput The input currency, either Ether or an ERC-20 * @template TOutput The output currency, 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 async exactOut( route: Route, amountOut: CurrencyAmount ): Promise> { return Trade.fromRoute(route, amountOut, TradeType.EXACT_OUTPUT) } /** * Constructs a trade by simulating swaps through the given route * @template TInput The input currency, either Ether or an ERC-20. * @template TOutput The output currency, 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 async fromRoute( route: Route, amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount, tradeType: TTradeType ): Promise> { let inputAmount: CurrencyAmount let outputAmount: CurrencyAmount if (tradeType === TradeType.EXACT_INPUT) { invariant(amount.currency.equals(route.input), 'INPUT') // Account for trades that wrap/unwrap as a first step let tokenAmount: CurrencyAmount = amountWithPathCurrency(amount, route.pools[0]) for (let i = 0; i < route.pools.length; i++) { const pool = route.pools[i] ;[tokenAmount] = await pool.getOutputAmount(tokenAmount) } inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator) outputAmount = CurrencyAmount.fromFractionalAmount(route.output, tokenAmount.numerator, tokenAmount.denominator) } else { invariant(amount.currency.equals(route.output), 'OUTPUT') // Account for trades that wrap/unwrap as a last step let tokenAmount: CurrencyAmount = amountWithPathCurrency(amount, route.pools[route.pools.length - 1]) for (let i = route.pools.length - 1; i >= 0; i--) { const pool = route.pools[i] ;[tokenAmount] = await pool.getInputAmount(tokenAmount) // Special case: if this is the last pool (first in backward iteration) and it's an ETH-WETH pool // with ETH as the route output, we need to convert the WETH amount back to ETH // so the next pool in the iteration can work with ETH // and vice versa if the route output is WETH if (i === route.pools.length - 1) { // Check if this is an ETH-WETH pool const isEthWethPool = pool.currency1.equals(pool.currency0.wrapped) if (isEthWethPool) { if (route.output.isNative && route.pools[i - 1]?.currency0.isNative) { // Convert WETH amount to ETH for the next pool tokenAmount = CurrencyAmount.fromFractionalAmount( pool.currency0, // native currency tokenAmount.numerator, tokenAmount.denominator ) } else if ( route.output.equals(pool.currency1) && route.pools[i - 1] && !route.pools[i - 1].currency0.isNative ) { // Convert ETH amount to WETH for the next pool tokenAmount = CurrencyAmount.fromFractionalAmount( pool.currency1, // wrapped currency tokenAmount.numerator, tokenAmount.denominator ) } } } } inputAmount = CurrencyAmount.fromFractionalAmount(route.input, tokenAmount.numerator, tokenAmount.denominator) outputAmount = CurrencyAmount.fromFractionalAmount(route.output, amount.numerator, amount.denominator) } return new Trade({ routes: [{ inputAmount, outputAmount, route }], tradeType, }) } /** * Constructs a trade from routes by simulating swaps * * @template TInput The input currency, either Ether or an ERC-20. * @template TOutput The output currency, 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 async fromRoutes( routes: { amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount route: Route }[], tradeType: TTradeType ): Promise> { const swaps: { route: Route inputAmount: CurrencyAmount outputAmount: CurrencyAmount }[] = await Promise.all( routes.map(async ({ amount, route }) => { const trade = await Trade.fromRoute(route, amount, tradeType) return trade.swaps[0] }) ) return new Trade({ routes: swaps, 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 currency, either Ether or an ERC-20 * @template TOutput The output currency, 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: { route: Route inputAmount: CurrencyAmount outputAmount: CurrencyAmount tradeType: TTradeType }): Trade { return new Trade({ ...constructorArguments, routes: [ { 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 currency, either Ether or an ERC-20 * @template TOutput The output currency, 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: { route: Route inputAmount: CurrencyAmount outputAmount: CurrencyAmount }[] tradeType: TTradeType }): Trade { 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: { route: Route inputAmount: CurrencyAmount outputAmount: CurrencyAmount }[] 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 poolIDSet = new Set() for (const { route } of routes) { for (const pool of route.pools) { poolIDSet.add(Pool.getPoolId(pool.currency0, pool.currency1, pool.fee, pool.tickSpacing, pool.hooks)) } } invariant(numPools === poolIDSet.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 { invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE') if (this.tradeType === TradeType.EXACT_OUTPUT) { return amountOut } else { const slippageAdjustedAmountOut = new Fraction(ONE) .subtract(slippageTolerance) .multiply(amountOut.quotient).quotient const clampedAmount = JSBI.greaterThan(slippageAdjustedAmountOut, JSBI.BigInt(0)) ? slippageAdjustedAmountOut : JSBI.BigInt(0) return CurrencyAmount.fromRawAmount(amountOut.currency, clampedAmount) } } /** * 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 { 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 { return new Price( this.inputAmount.currency, this.outputAmount.currency, this.maximumAmountIn(slippageTolerance).quotient, this.minimumAmountOut(slippageTolerance).quotient ) } /** * Given a list of pools, and a fixed amount in, returns the top `maxNumResults` trades that go from an input currency * amount to an output currency, 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 pools the pools to consider in finding the best trade * @param nextAmountIn 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 pool * @param currentPools used in recursion; the current list of pools * @param currencyAmountIn used in recursion; the original value of the currencyAmountIn parameter * @param bestTrades used in recursion; the current list of best trades * @returns The exact in trade */ public static async bestTradeExactIn( pools: Pool[], currencyAmountIn: CurrencyAmount, currencyOut: TOutput, { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, // used in recursion. currentPools: Pool[] = [], nextAmountIn: CurrencyAmount = currencyAmountIn, bestTrades: Trade[] = [] ): Promise[]> { invariant(pools.length > 0, 'POOLS') invariant(maxHops > 0, 'MAX_HOPS') invariant(currencyAmountIn === nextAmountIn || currentPools.length > 0, 'INVALID_RECURSION') const amountIn = nextAmountIn for (let i = 0; i < pools.length; i++) { const pool = pools[i] // pool irrelevant if (!pool.currency0.equals(amountIn.currency) && !pool.currency1.equals(amountIn.currency)) continue let amountOut: CurrencyAmount try { ;[amountOut] = await pool.getOutputAmount(amountIn) } catch (error) { // input too low if ((error as any).isInsufficientInputAmountError) { continue } throw error } // we have arrived at the output currency, so this is the final trade of one of the paths if (amountOut.currency.equals(currencyOut)) { sortedInsert( bestTrades, await Trade.fromRoute( new Route([...currentPools, pool], currencyAmountIn.currency, currencyOut), currencyAmountIn, TradeType.EXACT_INPUT ), maxNumResults, tradeComparator ) } else if (maxHops > 1 && pools.length > 1) { const poolsExcludingThisPool = pools.slice(0, i).concat(pools.slice(i + 1, pools.length)) // otherwise, consider all the other paths that lead from this currency as long as we have not exceeded maxHops await Trade.bestTradeExactIn( poolsExcludingThisPool, currencyAmountIn, currencyOut, { maxNumResults, maxHops: maxHops - 1, }, [...currentPools, pool], amountOut, bestTrades ) } } return bestTrades } /** * similar to the above method but instead targets a fixed output amount * given a list of pools, and a fixed amount out, returns the top `maxNumResults` trades that go from an input currency * to an output currency 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 pools the pools to consider in finding the best trade * @param currencyIn the currency to spend * @param currencyAmountOut the desired currency amount out * @param nextAmountOut 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 pool * @param currentPools used in recursion; the current list of pools * @param bestTrades used in recursion; the current list of best trades * @returns The exact out trade */ public static async bestTradeExactOut( pools: Pool[], currencyIn: TInput, currencyAmountOut: CurrencyAmount, { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, // used in recursion. currentPools: Pool[] = [], nextAmountOut: CurrencyAmount = currencyAmountOut, bestTrades: Trade[] = [] ): Promise[]> { invariant(pools.length > 0, 'POOLS') invariant(maxHops > 0, 'MAX_HOPS') invariant(currencyAmountOut === nextAmountOut || currentPools.length > 0, 'INVALID_RECURSION') const amountOut = nextAmountOut for (let i = 0; i < pools.length; i++) { const pool = pools[i] // pool irrelevant if (!pool.currency0.equals(amountOut.currency) && !pool.currency1.equals(amountOut.currency)) continue let amountIn: CurrencyAmount try { ;[amountIn] = await pool.getInputAmount(amountOut) } catch (error) { // not enough liquidity in this pool if ((error as any).isInsufficientReservesError) { continue } throw error } // we have arrived at the input currency, so this is the first trade of one of the paths if (amountIn.currency.equals(currencyIn)) { sortedInsert( bestTrades, await Trade.fromRoute( new Route([pool, ...currentPools], currencyIn, currencyAmountOut.currency), currencyAmountOut, TradeType.EXACT_OUTPUT ), maxNumResults, tradeComparator ) } else if (maxHops > 1 && pools.length > 1) { const poolsExcludingThisPool = pools.slice(0, i).concat(pools.slice(i + 1, pools.length)) // otherwise, consider all the other paths that arrive at this currency as long as we have not exceeded maxHops await Trade.bestTradeExactOut( poolsExcludingThisPool, currencyIn, currencyAmountOut, { maxNumResults, maxHops: maxHops - 1, }, [pool, ...currentPools], amountIn, bestTrades ) } } return bestTrades } }