UNPKG

@uniswap/v4-sdk

Version:

⚒️ An SDK for building applications on top of Uniswap V4

430 lines 21.6 kB
import { Fraction, Percent, Price, sortedInsert, CurrencyAmount, TradeType } from '@uniswap/sdk-core'; 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, b) { // 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; } } } /** * 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. */ get route() { invariant(this.swaps.length === 1, 'MULTIPLE_ROUTES'); return this.swaps[0].route; } /** * The input amount for the trade assuming no slippage. */ get inputAmount() { 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 output amount for the trade assuming no slippage. */ get outputAmount() { 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 price expressed in terms of output amount/input amount. */ get executionPrice() { var _a; return ((_a = this._executionPrice) !== null && _a !== void 0 ? _a : (this._executionPrice = new Price(this.inputAmount.currency, this.outputAmount.currency, this.inputAmount.quotient, this.outputAmount.quotient))); } /** * Returns the percent difference between the route's mid price and the price impact */ get priceImpact() { 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 */ static async exactIn(route, amountIn) { 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 */ static async exactOut(route, amountOut) { 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 */ static async fromRoute(route, amount, tradeType) { var _a; let inputAmount; let outputAmount; if (tradeType === TradeType.EXACT_INPUT) { invariant(amount.currency.equals(route.input), 'INPUT'); // Account for trades that wrap/unwrap as a first step let tokenAmount = 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 = 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 && ((_a = route.pools[i - 1]) === null || _a === void 0 ? void 0 : _a.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 */ static async fromRoutes(routes, tradeType) { const swaps = 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 */ static createUncheckedTrade(constructorArguments) { 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 */ static createUncheckedTradeWithMultipleRoutes(constructorArguments) { 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 */ constructor({ routes, tradeType, }) { 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 */ minimumAmountOut(slippageTolerance, amountOut = this.outputAmount) { 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 */ maximumAmountIn(slippageTolerance, amountIn = this.inputAmount) { 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 */ worstExecutionPrice(slippageTolerance) { 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 */ static async bestTradeExactIn(pools, currencyAmountIn, currencyOut, { maxNumResults = 3, maxHops = 3 } = {}, // used in recursion. currentPools = [], nextAmountIn = currencyAmountIn, bestTrades = []) { 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; try { ; [amountOut] = await pool.getOutputAmount(amountIn); } catch (error) { // input too low if (error.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 */ static async bestTradeExactOut(pools, currencyIn, currencyAmountOut, { maxNumResults = 3, maxHops = 3 } = {}, // used in recursion. currentPools = [], nextAmountOut = currencyAmountOut, bestTrades = []) { 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; try { ; [amountIn] = await pool.getInputAmount(amountOut); } catch (error) { // not enough liquidity in this pool if (error.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; } } //# sourceMappingURL=trade.js.map