From 1bc5a60261499535122ddf5ede2e7aac0a474912 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Fri, 17 Sep 2021 10:02:15 -0400 Subject: [PATCH] routeToRatio endpoint + CLI refactor (#4) * restructure CLI for multi-command * routeToRatio * new entrypoint: quote-to-ratio --- README.md | 21 +- bin/cli | 5 +- scripts/cli.ts => cli/base-command.ts | 266 +++++++----------- cli/commands/quote-to-ratio.ts | 159 +++++++++++ cli/commands/quote.ts | 159 +++++++++++ .../types/bunyan-debug-stream/index.d.ts | 0 package.json | 3 + src/providers/quote-provider.ts | 8 +- src/routers/alpha-router/alpha-router.ts | 60 +++- .../functions/calculate-ratio-amount-in.ts | 21 ++ src/routers/router.ts | 12 +- .../routers/alpha-router/alpha-router.test.ts | 132 ++++++++- .../calculate-ratio-amount-in.test.ts | 218 ++++++++++++++ test/unit/test-util/mock-data.ts | 9 + 14 files changed, 901 insertions(+), 172 deletions(-) rename scripts/cli.ts => cli/base-command.ts (52%) create mode 100644 cli/commands/quote-to-ratio.ts create mode 100644 cli/commands/quote.ts rename {scripts => cli}/types/bunyan-debug-stream/index.d.ts (100%) create mode 100644 src/routers/alpha-router/functions/calculate-ratio-amount-in.ts create mode 100644 test/unit/routers/alpha-router/functions/calculate-ratio-amount-in.test.ts diff --git a/README.md b/README.md index 4eeea1e07..fcf990976 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ JSON_RPC_PROVIDER = '' Then from the root directory you can execute the CLI. ``` -./bin/cli --tokenIn 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --tokenOut 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 --amount 1000 --exactIn --recipient 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B +./bin/cli quote --tokenIn 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --tokenOut 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 --amount 1000 --exactIn --recipient 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B Best Route: 100.00% = USDC -- 0.3% --> UNI @@ -41,4 +41,23 @@ Value: 0x00 blockNumber: "13088815" estimatedGasUsed: "113000" gasPriceWei: "130000000000" + + +./bin/cli quote-to-ratio --token0 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --token1 0xdac17f958d2ee523a2206206994597c13d831ec7 --feeAmount 3000 --recipient 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B --token0Balance 1000 --token1Balance 2000 --tickLower -120 --tickUpper 120 + +Best Route: +100.00% = USDT -- 0.05% --> USDC +Raw Quote Exact In: + 392.68 +Gas Adjusted Quote In}: + 346.13 + +Gas Used Quote Token: 46.550010 +Gas Used USD: 46.342899 +Calldata: 0x414bf389000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ab5801a7d398351b8be11c439e05c5b3259aec9b000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000176a736c000000000000000000000000000000000000000000000000000000001764f8650000000000000000000000000000000000000000000000000000000000000000 + Value: 0x00 + + blockNumber: "13239188" + estimatedGasUsed: "113000" + gasPriceWei: "116690684398" ``` diff --git a/bin/cli b/bin/cli index 9fc47ed77..d3bdaae58 100755 --- a/bin/cli +++ b/bin/cli @@ -12,6 +12,5 @@ if (dev) { }); } -require(`../scripts/cli`) - .UniswapSORCLI.run() - .catch(require("@oclif/errors/handle")); +require('@oclif/command').run() + .catch(require('@oclif/errors/handle')) diff --git a/scripts/cli.ts b/cli/base-command.ts similarity index 52% rename from scripts/cli.ts rename to cli/base-command.ts index 6565d9c64..59f204573 100644 --- a/scripts/cli.ts +++ b/cli/base-command.ts @@ -1,11 +1,12 @@ /// import { Command, flags } from '@oclif/command'; +import { ParserOutput } from '@oclif/parser/lib/parse'; import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'; -import { Currency, Ether, Percent } from '@uniswap/sdk-core'; +import { Currency, CurrencyAmount } from '@uniswap/sdk-core'; +import { MethodParameters } from '@uniswap/v3-sdk'; import { default as bunyan, default as Logger } from 'bunyan'; import bunyanDebugStream from 'bunyan-debug-stream'; -import dotenv from 'dotenv'; -import { ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; import { AlphaRouter, CachingGasStationProvider, @@ -17,40 +18,27 @@ import { HeuristicGasModelFactory, ID_TO_CHAIN_ID, ID_TO_NETWORK_NAME, + IPoolProvider, IRouter, + ISwapToRatio, + ITokenProvider, LegacyRouter, MetricLogger, - parseAmount, PoolProvider, QuoteProvider, routeAmountsToString, + RouteWithValidQuote, setGlobalLogger, setGlobalMetric, SubgraphProvider, - SwapRoute, TokenListProvider, TokenProvider, TokenProviderWithFallback, UniswapMulticallProvider, } from '../src'; -dotenv.config(); - -ethers.utils.Logger.globalLogger(); -ethers.utils.Logger.setLogLevel(ethers.utils.Logger.levels.DEBUG); - -export class UniswapSORCLI extends Command { - static description = 'Uniswap Smart Order Router CLI'; - +export abstract class BaseCommand extends Command { static flags = { - version: flags.version({ char: 'v' }), - help: flags.help({ char: 'h' }), - tokenIn: flags.string({ char: 'i', required: true }), - tokenOut: flags.string({ char: 'o', required: true }), - recipient: flags.string({ required: true }), - amount: flags.string({ char: 'a', required: true }), - exactIn: flags.boolean({ required: false }), - exactOut: flags.boolean({ required: false }), topN: flags.integer({ required: false, default: 3, @@ -91,11 +79,6 @@ export class UniswapSORCLI extends Command { required: false, default: 5, }), - router: flags.string({ - char: 's', - required: false, - default: 'alpha', - }), chainId: flags.integer({ char: 'c', required: false, @@ -105,42 +88,74 @@ export class UniswapSORCLI extends Command { tokenListURI: flags.string({ required: false, }), + router: flags.string({ + char: 's', + required: false, + default: 'alpha', + }), debug: flags.boolean(), debugJSON: flags.boolean(), }; - async run() { - const { flags } = this.parse(UniswapSORCLI); + private _log: Logger | null = null; + private _router: IRouter | null = null; + private _swapToRatioRouter: ISwapToRatio | null = null; + private _tokenProvider: ITokenProvider | null = null; + private _poolProvider: IPoolProvider | null = null; + + get logger() { + return this._log + ? this._log + : bunyan.createLogger({ + name: 'Default Logger', + }); + } + + get router() { + if (this._router) { + return this._router; + } else { + throw 'router not initialized'; + } + } + + get swapToRatioRouter() { + if (this._swapToRatioRouter) { + return this._swapToRatioRouter; + } else { + throw 'swapToRatioRouter not initialized'; + } + } + + get tokenProvider() { + if (this._tokenProvider) { + return this._tokenProvider; + } else { + throw 'tokenProvider not initialized'; + } + } + + get poolProvider() { + if (this._poolProvider) { + return this._poolProvider; + } else { + throw 'poolProvider not initialized'; + } + } + + async init() { + const query: ParserOutput = this.parse(); const { - tokenIn: tokenInStr, - tokenOut: tokenOutStr, chainId: chainIdNumb, router: routerStr, - amount: amountStr, - exactIn, - exactOut, - recipient, - tokenListURI, debug, debugJSON, - topN, - topNTokenInOut, - topNSecondHop, - topNWithEachBaseToken, - topNWithBaseToken, - topNWithBaseTokenInSet, - maxSwapsPerPath, - minSplits, - maxSplits, - distributionPercent, - } = flags; - - if ((exactIn && exactOut) || (!exactIn && !exactOut)) { - throw new Error('Must set either --exactIn or --exactOut.'); - } + tokenListURI, + } = query.flags; + // initialize logger const logLevel = debug || debugJSON ? bunyan.DEBUG : bunyan.INFO; - const log: Logger = bunyan.createLogger({ + this._log = bunyan.createLogger({ name: 'Uniswap Smart Order Router', serializers: bunyan.stdSerializers, level: logLevel, @@ -163,7 +178,7 @@ export class UniswapSORCLI extends Command { }); if (debug || debugJSON) { - setGlobalLogger(log); + setGlobalLogger(this.logger); } const metricLogger: MetricLogger = new MetricLogger(); @@ -189,55 +204,41 @@ export class UniswapSORCLI extends Command { DEFAULT_TOKEN_LIST ); } + const multicall2Provider = new UniswapMulticallProvider(chainId, provider); + this._poolProvider = new PoolProvider(chainId, multicall2Provider); + // initialize tokenProvider const tokenProviderOnChain = new TokenProvider(chainId, multicall2Provider); - const tokenProvider = new TokenProviderWithFallback( + this._tokenProvider = new TokenProviderWithFallback( chainId, tokenListProvider, tokenProviderOnChain ); - const tokenAccessor = await tokenProvider.getTokens([ - tokenInStr, - tokenOutStr, - ]); - - const tokenIn: Currency = - tokenInStr == 'ETH' - ? Ether.onChain(chainId) - : tokenAccessor.getTokenByAddress(tokenInStr)!; - const tokenOut: Currency = - tokenOutStr == 'ETH' - ? Ether.onChain(chainId) - : tokenAccessor.getTokenByAddress(tokenOutStr)!; - - const multicall = new UniswapMulticallProvider(chainId, provider); - - let router: IRouter; if (routerStr == 'legacy') { - router = new LegacyRouter({ + this._router = new LegacyRouter({ chainId, multicall2Provider, poolProvider: new PoolProvider(chainId, multicall2Provider), quoteProvider: new QuoteProvider(chainId, provider, multicall2Provider), - tokenProvider, + tokenProvider: this.tokenProvider, }); } else { - router = new AlphaRouter({ + const router = new AlphaRouter({ provider, chainId, - subgraphProvider: new CachingSubgraphProvider(chainId, + subgraphProvider: new CachingSubgraphProvider(chainId, new SubgraphProvider(chainId, undefined, 10000) ), - multicall2Provider: multicall, + multicall2Provider: multicall2Provider, poolProvider: new CachingPoolProvider(chainId, new PoolProvider(chainId, multicall2Provider) ), quoteProvider: new QuoteProvider( chainId, provider, - multicall, + multicall2Provider, { retries: 2, minTimeout: 25, @@ -253,95 +254,40 @@ export class UniswapSORCLI extends Command { new EIP1559GasPriceProvider(provider) ), gasModelFactory: new HeuristicGasModelFactory(), - tokenProvider: tokenProvider, + tokenProvider: this.tokenProvider, }); - } - - let swapRoutes: SwapRoute | null; - if (exactIn) { - const amountIn = parseAmount(amountStr, tokenIn); - //const amountIn = CurrencyAmount.fromRawAmount(tokenIn, amountStr); - swapRoutes = await router.routeExactIn( - tokenIn, - tokenOut, - amountIn, - { - deadline: 100, - recipient, - slippageTolerance: new Percent(5, 10_000), - }, - { - topN, - topNTokenInOut, - topNSecondHop, - topNWithEachBaseToken, - topNWithBaseToken, - topNWithBaseTokenInSet, - maxSwapsPerPath, - minSplits, - maxSplits, - distributionPercent, - } - ); - } else { - const amountOut = parseAmount(amountStr, tokenOut); - swapRoutes = await router.routeExactOut( - tokenIn, - tokenOut, - amountOut, - { - deadline: 100, - recipient, - slippageTolerance: new Percent(5, 10_000), - }, - { - topN, - topNTokenInOut, - topNSecondHop, - topNWithEachBaseToken, - topNWithBaseToken, - topNWithBaseTokenInSet, - maxSwapsPerPath, - minSplits, - maxSplits, - distributionPercent, - } - ); - } - if (!swapRoutes) { - log.error( - `Could not find route. ${ - debug ? '' : 'Run in debug mode for more info' - }.` - ); - return; + this._swapToRatioRouter = router; + this._router = router; } + } - const { - blockNumber, - estimatedGasUsed, - estimatedGasUsedQuoteToken, - estimatedGasUsedUSD, - gasPriceWei, - methodParameters, - quote, - quoteGasAdjusted, - route: routeAmounts, - } = swapRoutes; - log.info(`Best Route:`); - log.info(`${routeAmountsToString(routeAmounts)}`); + logSwapResults( + routeAmounts: RouteWithValidQuote[], + quote: CurrencyAmount, + quoteGasAdjusted: CurrencyAmount, + estimatedGasUsedQuoteToken: CurrencyAmount, + estimatedGasUsedUSD: CurrencyAmount, + methodParameters: MethodParameters | undefined, + blockNumber: BigNumber, + estimatedGasUsed: BigNumber, + gasPriceWei: BigNumber + ) { + this.logger.info(`Best Route:`); + this.logger.info(`${routeAmountsToString(routeAmounts)}`); - log.info(`\tRaw Quote ${exactIn ? 'Out' : 'In'}:`); - log.info(`\t\t${quote.toFixed(2)}`); - log.info(`\tGas Adjusted Quote ${exactIn ? 'Out' : 'In'}:`); - log.info(`\t\t${quoteGasAdjusted.toFixed(2)}`); - log.info(``); - log.info(`Gas Used Quote Token: ${estimatedGasUsedQuoteToken.toFixed(6)}`); - log.info(`Gas Used USD: ${estimatedGasUsedUSD.toFixed(6)}`); - log.info(`Calldata: ${methodParameters?.calldata}`); - log.info(`Value: ${methodParameters?.value}`); - log.info({ + this.logger.info(`\tRaw Quote Exact In:`); + this.logger.info(`\t\t${quote.toFixed(2)}`); + this.logger.info(`\tGas Adjusted Quote In}:`); + this.logger.info(`\t\t${quoteGasAdjusted.toFixed(2)}`); + this.logger.info(``); + this.logger.info( + `Gas Used Quote Token: ${estimatedGasUsedQuoteToken.toFixed(6)}` + ); + this.logger.info(`Gas Used USD: ${estimatedGasUsedUSD.toFixed(6)}`); + this.logger.info(`Calldata: ${methodParameters?.calldata}`); + this.logger.info(`Value: ${methodParameters?.value}`); + this.logger.info({ blockNumber: blockNumber.toString(), estimatedGasUsed: estimatedGasUsed.toString(), gasPriceWei: gasPriceWei.toString(), diff --git a/cli/commands/quote-to-ratio.ts b/cli/commands/quote-to-ratio.ts new file mode 100644 index 000000000..59c6fa716 --- /dev/null +++ b/cli/commands/quote-to-ratio.ts @@ -0,0 +1,159 @@ +import { flags } from '@oclif/command'; +import { Currency, Ether, Percent } from '@uniswap/sdk-core'; +import { Position } from '@uniswap/v3-sdk'; +import dotenv from 'dotenv'; +import { ethers } from 'ethers'; +import { ID_TO_CHAIN_ID, parseAmount, SwapRoute } from '../../src'; +import { BaseCommand } from '../base-command'; + +dotenv.config(); + +ethers.utils.Logger.globalLogger(); +ethers.utils.Logger.setLogLevel(ethers.utils.Logger.levels.DEBUG); + +export class QuoteToRatio extends BaseCommand { + static description = 'Uniswap Smart Order Router CLI'; + + static flags = { + ...BaseCommand.flags, + version: flags.version({ char: 'v' }), + help: flags.help({ char: 'h' }), + token0: flags.string({ char: 'i', required: true }), + token1: flags.string({ char: 'o', required: true }), + feeAmount: flags.integer({ char: 'f', required: true }), + token0Balance: flags.string({ required: true }), + token1Balance: flags.string({ required: true }), + recipient: flags.string({ required: true }), + tickLower: flags.integer({ required: true }), + tickUpper: flags.integer({ required: true }), + }; + + async run() { + const { flags } = this.parse(QuoteToRatio); + const { + chainId: chainIdNumb, + token0: token0Str, + token1: token1Str, + token0Balance: token0BalanceStr, + token1Balance: token1BalanceStr, + feeAmount, + tickLower, + tickUpper, + recipient, + debug, + topN, + topNTokenInOut, + topNSecondHop, + topNWithEachBaseToken, + topNWithBaseToken, + topNWithBaseTokenInSet, + maxSwapsPerPath, + maxSplits, + distributionPercent, + } = flags; + + const log = this.logger; + const router = this.swapToRatioRouter; + const tokenProvider = this.tokenProvider; + + const tokenAccessor = await tokenProvider.getTokens([ + token0Str, + token1Str, + ]); + + const chainId = ID_TO_CHAIN_ID(chainIdNumb); + const tokenIn: Currency = + token0Str == 'ETH' + ? Ether.onChain(chainId) + : tokenAccessor.getTokenByAddress(token0Str)!; + const tokenOut: Currency = + token1Str == 'ETH' + ? Ether.onChain(chainId) + : tokenAccessor.getTokenByAddress(token1Str)!; + + const tokenInBalance = parseAmount(token0BalanceStr, tokenIn); + const tokenOutBalance = parseAmount(token1BalanceStr, tokenOut); + + const poolAccessor = await this.poolProvider.getPools([ + [tokenIn.wrapped, tokenOut.wrapped, 3000], + ]); + + const pool = poolAccessor?.getPool( + tokenIn.wrapped, + tokenOut.wrapped, + feeAmount + ); + if (!pool) { + log.error( + `Could not find pool. ${ + debug ? '' : 'Run in debug mode for more info' + }.` + ); + return; + } + + const position = new Position({ + pool, + tickUpper, + tickLower, + liquidity: 1, + }); + + let swapRoutes: SwapRoute | null; + swapRoutes = await router?.routeToRatio( + tokenInBalance, + tokenOutBalance, + position, + { + deadline: 100, + recipient, + slippageTolerance: new Percent(5, 10_000), + }, + { + topN, + topNDirectSwaps: 2, + topNTokenInOut, + topNSecondHop, + topNWithEachBaseToken, + topNWithBaseToken, + topNWithBaseTokenInSet, + maxSwapsPerPath, + maxSplits, + distributionPercent, + } + ); + + if (!swapRoutes) { + log.error( + `Could not find route. ${ + debug ? '' : 'Run in debug mode for more info' + }.` + ); + return; + } + + const { + blockNumber, + estimatedGasUsed, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + gasPriceWei, + methodParameters, + quote, + quoteGasAdjusted, + route: routeAmounts, + } = swapRoutes; + + this.logSwapResults( + routeAmounts, + quote, + quoteGasAdjusted, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + methodParameters, + blockNumber, + estimatedGasUsed, + gasPriceWei + ); + } +} diff --git a/cli/commands/quote.ts b/cli/commands/quote.ts new file mode 100644 index 000000000..5927e5825 --- /dev/null +++ b/cli/commands/quote.ts @@ -0,0 +1,159 @@ +import { flags } from '@oclif/command'; +import { Currency, Ether, Percent } from '@uniswap/sdk-core'; +import dotenv from 'dotenv'; +import { ethers } from 'ethers'; +import { ID_TO_CHAIN_ID, parseAmount, SwapRoute } from '../../src'; +import { BaseCommand } from '../base-command'; + +dotenv.config(); + +ethers.utils.Logger.globalLogger(); +ethers.utils.Logger.setLogLevel(ethers.utils.Logger.levels.DEBUG); + +export class Quote extends BaseCommand { + static description = 'Uniswap Smart Order Router CLI'; + + static flags = { + ...BaseCommand.flags, + version: flags.version({ char: 'v' }), + help: flags.help({ char: 'h' }), + tokenIn: flags.string({ char: 'i', required: true }), + tokenOut: flags.string({ char: 'o', required: true }), + recipient: flags.string({ required: true }), + amount: flags.string({ char: 'a', required: true }), + exactIn: flags.boolean({ required: false }), + exactOut: flags.boolean({ required: false }), + }; + + async run() { + const { flags } = this.parse(Quote); + const { + tokenIn: tokenInStr, + tokenOut: tokenOutStr, + amount: amountStr, + exactIn, + exactOut, + recipient, + debug, + topN, + topNTokenInOut, + topNSecondHop, + topNWithEachBaseToken, + topNWithBaseToken, + topNWithBaseTokenInSet, + maxSwapsPerPath, + minSplits, + maxSplits, + distributionPercent, + chainId: chainIdNumb, + } = flags; + + if ((exactIn && exactOut) || (!exactIn && !exactOut)) { + throw new Error('Must set either --exactIn or --exactOut.'); + } + + const chainId = ID_TO_CHAIN_ID(chainIdNumb); + + const log = this.logger; + const tokenProvider = this.tokenProvider; + const router = this.router; + + const tokenAccessor = await tokenProvider.getTokens([ + tokenInStr, + tokenOutStr, + ]); + + const tokenIn: Currency = + tokenInStr == 'ETH' + ? Ether.onChain(chainId) + : tokenAccessor.getTokenByAddress(tokenInStr)!; + const tokenOut: Currency = + tokenOutStr == 'ETH' + ? Ether.onChain(chainId) + : tokenAccessor.getTokenByAddress(tokenOutStr)!; + + let swapRoutes: SwapRoute | null; + if (exactIn) { + const amountIn = parseAmount(amountStr, tokenIn); + swapRoutes = await router.routeExactIn( + tokenIn, + tokenOut, + amountIn, + { + deadline: 100, + recipient, + slippageTolerance: new Percent(5, 10_000), + }, + { + topN, + topNTokenInOut, + topNSecondHop, + topNWithEachBaseToken, + topNWithBaseToken, + topNWithBaseTokenInSet, + maxSwapsPerPath, + minSplits, + maxSplits, + distributionPercent, + } + ); + } else { + const amountOut = parseAmount(amountStr, tokenOut); + swapRoutes = await router.routeExactOut( + tokenIn, + tokenOut, + amountOut, + { + deadline: 100, + recipient, + slippageTolerance: new Percent(5, 10_000), + }, + { + topN, + topNTokenInOut, + topNSecondHop, + topNWithEachBaseToken, + topNWithBaseToken, + topNWithBaseTokenInSet, + maxSwapsPerPath, + minSplits, + maxSplits, + distributionPercent, + } + ); + } + + if (!swapRoutes) { + log.error( + `Could not find route. ${ + debug ? '' : 'Run in debug mode for more info' + }.` + ); + return; + } + + const { + blockNumber, + estimatedGasUsed, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + gasPriceWei, + methodParameters, + quote, + quoteGasAdjusted, + route: routeAmounts, + } = swapRoutes; + + this.logSwapResults( + routeAmounts, + quote, + quoteGasAdjusted, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + methodParameters, + blockNumber, + estimatedGasUsed, + gasPriceWei + ); + } +} diff --git a/scripts/types/bunyan-debug-stream/index.d.ts b/cli/types/bunyan-debug-stream/index.d.ts similarity index 100% rename from scripts/types/bunyan-debug-stream/index.d.ts rename to cli/types/bunyan-debug-stream/index.d.ts diff --git a/package.json b/package.json index 069f09500..f127517fa 100644 --- a/package.json +++ b/package.json @@ -97,5 +97,8 @@ ], "prettier": { "singleQuote": true + }, + "oclif": { + "commands": "./cli/commands" } } diff --git a/src/providers/quote-provider.ts b/src/providers/quote-provider.ts index 1aaf724b6..90f88da50 100644 --- a/src/providers/quote-provider.ts +++ b/src/providers/quote-provider.ts @@ -117,7 +117,7 @@ export class QuoteProvider implements IQuoteProvider { protected quoterAddressOverride?: string, ) { const quoterAddress = quoterAddressOverride ? quoterAddressOverride : chainToQuoterAddress[this.chainId]; - + if (!quoterAddress) { throw new Error(`No address for Uniswap Multicall Contract on chain id: ${chainId}`); } @@ -228,8 +228,8 @@ export class QuoteProvider implements IQuoteProvider { const [success, failed, pending] = this.partitionQuotes(quoteStates); log.info( - `Starting attempt: ${attemptNumber}. - Currently ${success.length} success, ${failed.length} failed, ${pending.length} pending. + `Starting attempt: ${attemptNumber}. + Currently ${success.length} success, ${failed.length} failed, ${pending.length} pending. Gas limit override: ${gasLimitOverride} Block number override: ${await providerConfig.blockNumber}.` ); @@ -281,7 +281,7 @@ export class QuoteProvider implements IQuoteProvider { inputs, results, } as QuoteBatchSuccess; - } catch (err) { + } catch (err: any) { // Error from providers have huge messages that include all the calldata and fill the logs. // Catch them and rethrow with shorter message. if (err.message.includes('header not found')) { diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 04c6b7d85..80b26dd96 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -2,9 +2,12 @@ import { Currency, Fraction, TradeType } from '@uniswap/sdk-core'; import { MethodParameters, Pool, + Position, Route, SwapRouter, + TickMath, Trade, + SqrtPriceMath } from '@uniswap/v3-sdk'; import { BigNumber, providers } from 'ethers'; import _ from 'lodash'; @@ -22,15 +25,17 @@ import { CurrencyAmount } from '../../util/amounts'; import { ChainId } from '../../util/chains'; import { log } from '../../util/log'; import { metric, MetricLoggerUnit } from '../../util/metric'; -import { IRouter, SwapConfig, SwapRoute } from '../router'; +import { IRouter, ISwapToRatio, SwapConfig, SwapRoute } from '../router'; import { RouteWithValidQuote } from './entities/route-with-valid-quote'; import { getBestSwapRoute } from './functions/best-swap-route'; import { computeAllRoutes } from './functions/compute-all-routes'; +import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in'; import { CandidatePoolsBySelectionCriteria, getCandidatePools, } from './functions/get-candidate-pools'; import { IGasModelFactory } from './gas-models/gas-model'; +import JSBI from 'jsbi' export type AlphaRouterParams = { chainId: ChainId; @@ -74,7 +79,7 @@ export const DEFAULT_CONFIG: AlphaRouterConfig = { distributionPercent: 5, }; -export class AlphaRouter implements IRouter { +export class AlphaRouter implements IRouter, ISwapToRatio { protected chainId: ChainId; protected provider: providers.BaseProvider; protected multicall2Provider: IMulticallProvider; @@ -110,6 +115,57 @@ export class AlphaRouter implements IRouter { this.gasModelFactory = gasModelFactory; } + public async routeToRatio( + token0Balance: CurrencyAmount, + token1Balance: CurrencyAmount, + position: Position, + swapConfig?: SwapConfig, + routingConfig = DEFAULT_CONFIG + ): Promise | null> { + if ( + token0Balance.currency.wrapped.address.toLowerCase() + > token1Balance.currency.wrapped.address.toLowerCase() + ) { + [token0Balance, token1Balance] = [token1Balance, token0Balance] + } + + const precision = JSBI.BigInt('1' + '0'.repeat(18)) + + const sqrtPriceX96 = position.pool.sqrtRatioX96 + const sqrtPriceX96Lower = TickMath.getSqrtRatioAtTick(position.tickLower) + const sqrtPriceX96Upper = TickMath.getSqrtRatioAtTick(position.tickUpper) + + const token0Proportion = SqrtPriceMath.getAmount0Delta( + sqrtPriceX96, + sqrtPriceX96Upper, + precision, + true + ) + const token1Proportion = SqrtPriceMath.getAmount1Delta( + sqrtPriceX96, + sqrtPriceX96Lower, + precision, + true + ) + + const amountToSwap = calculateRatioAmountIn( + new Fraction(token0Proportion, token1Proportion), + position.pool.token0Price, + token0Balance, + token1Balance, + ) + + return this.routeExactIn( + amountToSwap.currency, + amountToSwap.currency == token0Balance.currency + ? token1Balance.currency + : token0Balance.currency, + amountToSwap, + swapConfig, + routingConfig + ) + } + public async routeExactIn( currencyIn: Currency, currencyOut: Currency, diff --git a/src/routers/alpha-router/functions/calculate-ratio-amount-in.ts b/src/routers/alpha-router/functions/calculate-ratio-amount-in.ts new file mode 100644 index 000000000..0d552da07 --- /dev/null +++ b/src/routers/alpha-router/functions/calculate-ratio-amount-in.ts @@ -0,0 +1,21 @@ +import { Fraction } from '@uniswap/sdk-core'; +import { CurrencyAmount } from '../../../util/amounts'; + +export function calculateRatioAmountIn( + optimalRatio: Fraction, + token0Price: Fraction, + token0Balance: CurrencyAmount, + token1Balance: CurrencyAmount +): CurrencyAmount { + // formula: amountToSwap = (token0Balance - (optimalRatio * token1Balance)) / ((optimalRatio * token0Price) + 1)) + const amountToSwapRaw = new Fraction(token0Balance.quotient) + .subtract(optimalRatio.multiply(token1Balance.quotient)) + .divide(optimalRatio.multiply(token0Price).add(1)); + + const zeroForOne = !amountToSwapRaw.lessThan(0) + + return CurrencyAmount.fromRawAmount( + zeroForOne ? token0Balance.currency : token1Balance.currency, + zeroForOne ? amountToSwapRaw.quotient : amountToSwapRaw.multiply(-1).multiply(token0Price).quotient + ); +} diff --git a/src/routers/router.ts b/src/routers/router.ts index c94c7c7e4..0439fc581 100644 --- a/src/routers/router.ts +++ b/src/routers/router.ts @@ -1,5 +1,5 @@ import { Currency, Percent, Token, TradeType } from '@uniswap/sdk-core'; -import { MethodParameters, Route as RouteRaw, Trade } from '@uniswap/v3-sdk'; +import { MethodParameters, Position, Route as RouteRaw, Trade} from '@uniswap/v3-sdk'; import { BigNumber } from 'ethers'; import { CurrencyAmount } from '../util/amounts'; import { RouteWithValidQuote } from './alpha-router'; @@ -56,3 +56,13 @@ export abstract class IRouter { routingConfig?: RoutingConfig ): Promise | null>; } + +export abstract class ISwapToRatio { + abstract routeToRatio( + token0Balance: CurrencyAmount, + token1Balance: CurrencyAmount, + position: Position, + swapConfig?: SwapConfig, + routingConfig?: RoutingConfig + ): Promise | null>; +} diff --git a/test/unit/routers/alpha-router/alpha-router.test.ts b/test/unit/routers/alpha-router/alpha-router.test.ts index 676c190c3..e4ce86d9d 100644 --- a/test/unit/routers/alpha-router/alpha-router.test.ts +++ b/test/unit/routers/alpha-router/alpha-router.test.ts @@ -1,5 +1,5 @@ import { Fraction, Percent } from '@uniswap/sdk-core'; -import { Pool } from '@uniswap/v3-sdk'; +import { Pool, Position } from '@uniswap/v3-sdk'; import { BigNumber, providers } from 'ethers'; import _ from 'lodash'; import sinon from 'sinon'; @@ -12,6 +12,7 @@ import { ETHGasStationInfoProvider, HeuristicGasModelFactory, Multicall2Provider, + parseAmount, PoolProvider, QuoteProvider, RouteSOR, @@ -30,12 +31,14 @@ import { buildMockPoolAccessor, buildMockTokenAccessor, DAI_USDT_LOW, + DAI_USDT_MEDIUM, mockBlock, mockBlockBN, mockGasPriceWeiBN, poolToSubgraphPool, USDC_DAI_LOW, USDC_DAI_MEDIUM, + USDC_USDT_MEDIUM, USDC_WETH_LOW, WETH9_USDT_LOW, } from '../../test-util/mock-data'; @@ -84,6 +87,7 @@ describe('alpha router', () => { USDC_WETH_LOW, WETH9_USDT_LOW, DAI_USDT_LOW, + USDC_USDT_MEDIUM, ]; mockPoolProvider.getPools.resolves(buildMockPoolAccessor(mockPools)); mockPoolProvider.getPoolAddress.callsFake((tA, tB, fee) => ({ @@ -400,4 +404,130 @@ describe('alpha router', () => { expect(swap!.blockNumber.eq(mockBlockBN)).toBeTruthy(); }); }); + + describe('to ratio', () => { + describe('when token0Balance has excess tokens', () => { + test('calls routeExactIn with correct parameters', async () => { + const token0Balance = parseAmount('20', USDT); + const token1Balance = parseAmount('5', USDC); + + const position = new Position({ + pool: USDC_USDT_MEDIUM, + tickUpper: 120, + tickLower: -120, + liquidity: 1, + }); + + const spy = sinon.spy(alphaRouter, 'routeExactIn') + + await alphaRouter.routeToRatio( + token0Balance, + token1Balance, + position, + undefined, + ROUTING_CONFIG + ); + + const exactAmountInBalance = parseAmount('7.5', USDT) + + const exactInputParameters = spy.firstCall.args + expect(exactInputParameters[0]).toEqual(token0Balance.currency) + expect(exactInputParameters[1]).toEqual(token1Balance.currency) + expect(exactInputParameters[2]).toEqual(exactAmountInBalance) + }) + }) + + describe('when token1Balance has excess tokens', () => { + test('calls routeExactIn with correct parameters', async () => { + const token0Balance = parseAmount('5', USDT); + const token1Balance = parseAmount('20', USDC); + + const position = new Position({ + pool: USDC_USDT_MEDIUM, + tickUpper: 120, + tickLower: -120, + liquidity: 1, + }); + + const spy = sinon.spy(alphaRouter, 'routeExactIn') + + await alphaRouter.routeToRatio( + token0Balance, + token1Balance, + position, + undefined, + ROUTING_CONFIG + ); + + const exactAmountInBalance = parseAmount('7.5', USDC) + + const exactInputParameters = spy.firstCall.args + expect(exactInputParameters[0]).toEqual(token1Balance.currency) + expect(exactInputParameters[1]).toEqual(token0Balance.currency) + expect(exactInputParameters[2]).toEqual(exactAmountInBalance) + }) + }) + + describe('when token0 has more decimal places than token1', () => { + test('calls routeExactIn with correct parameters', async () => { + const token0Balance = parseAmount('20', DAI); + const token1Balance = parseAmount('5' + '0'.repeat(12), USDT); + + const position = new Position({ + pool: DAI_USDT_MEDIUM, + tickUpper: 120, + tickLower: -120, + liquidity: 1, + }); + + const spy = sinon.spy(alphaRouter, 'routeExactIn') + + await alphaRouter.routeToRatio( + token0Balance, + token1Balance, + position, + undefined, + ROUTING_CONFIG + ); + + const exactAmountInBalance = parseAmount('7.5', DAI) + + const exactInputParameters = spy.firstCall.args + expect(exactInputParameters[0]).toEqual(token0Balance.currency) + expect(exactInputParameters[1]).toEqual(token1Balance.currency) + expect(exactInputParameters[2]).toEqual(exactAmountInBalance) + }) + }) + + describe('when token1 has more decimal places than token0', () => { + test('calls routeExactIn with correct parameters', async () => { + const token0Balance = parseAmount('20' + '0'.repeat(12), USDC); + const token1Balance = parseAmount('5', WETH9[1]); + + const position = new Position({ + pool: USDC_WETH_LOW, + tickUpper: 120, + tickLower: -120, + liquidity: 1, + }); + + const spy = sinon.spy(alphaRouter, 'routeExactIn') + + await alphaRouter.routeToRatio( + token0Balance, + token1Balance, + position, + undefined, + ROUTING_CONFIG + ); + + const exactAmountInBalance = parseAmount('7500000000000', USDC) + + const exactInputParameters = spy.firstCall.args + expect(exactInputParameters[0]).toEqual(token0Balance.currency) + expect(exactInputParameters[1]).toEqual(token1Balance.currency) + expect(exactInputParameters[2]).toEqual(exactAmountInBalance) + }) + }) + }) }); diff --git a/test/unit/routers/alpha-router/functions/calculate-ratio-amount-in.test.ts b/test/unit/routers/alpha-router/functions/calculate-ratio-amount-in.test.ts new file mode 100644 index 000000000..18f813c62 --- /dev/null +++ b/test/unit/routers/alpha-router/functions/calculate-ratio-amount-in.test.ts @@ -0,0 +1,218 @@ +import { Fraction, Token } from '@uniswap/sdk-core'; +import { parseAmount } from '../../../../../src'; +import { calculateRatioAmountIn } from '../../../../../src/routers/alpha-router/functions/calculate-ratio-amount-in'; + +const ADDRESS_ZERO = `0x${'0'.repeat(40)}`; +const ADDRESS_ONE = `0x${'0'.repeat(39)}1`; + +describe('calculate ratio amount in', () => { + let token0: Token; + let token1: Token; + + beforeEach(() => { + token0 = new Token(1, ADDRESS_ZERO, 18, 'TEST1', 'Test Token 1'); + token1 = new Token(1, ADDRESS_ONE, 18, 'TEST2', 'Test Token 2'); + }); + + describe('when there is excess of token0', () => { + it('returns correct amountIn with simple inputs', () => { + const optimalRatio = new Fraction(1, 1); + const price = new Fraction(2, 1); + const token0Amount = parseAmount('20', token0); + const token1Amount = parseAmount('5', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token0Amount, + token1Amount + ); + + expect(amountIn.quotient.toString()).toEqual('5000000000000000000'); + expect(amountIn.currency).toEqual(token0Amount.currency); + }); + + it('returns correct amountIn when token0 has more decimal places', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(1, 2); + const token1SixDecimals = new Token( + 1, + ADDRESS_ZERO, + 6, + 'TEST1', + 'Test Token 1' + ); + const token0Amount = parseAmount('20', token0); + const token1Amount = parseAmount('5000000000000', token1SixDecimals); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token0Amount, + token1Amount + ); + + expect(amountIn.quotient.toString()).toEqual('14000000000000000000'); + expect(amountIn.currency).toEqual(token0Amount.currency); + }); + + it('returns correct amountIn when token1 has more decimal places', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(2, 1); + const token0SixDecimals = new Token( + 1, + ADDRESS_ZERO, + 6, + 'TEST1', + 'Test Token 1' + ); + const token0Amount = parseAmount('20000000000000', token0SixDecimals); + const token1Amount = parseAmount('5', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token0Amount, + token1Amount + ); + + expect(amountIn.quotient.toString()).toEqual('8750000000000000000'); + expect(amountIn.currency).toEqual(token0Amount.currency); + }); + + it('returns correct amountIn with price greater than 1', () => { + const optimalRatio = new Fraction(2, 1); + const price = new Fraction(2, 1); + const token0Amount = parseAmount('20', token0); + const token1Amount = parseAmount('5', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token0Amount, + token1Amount + ); + + expect(amountIn.quotient.toString()).toEqual('2000000000000000000'); + expect(amountIn.currency).toEqual(token0Amount.currency); + }); + + it('returns correct amountIn when price is less than 1', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(1, 2); + const token0Amount = parseAmount('20', token0); + const token1Amount = parseAmount('5', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token0Amount, + token1Amount + ); + + expect(amountIn.quotient.toString()).toEqual('14000000000000000000'); + expect(amountIn.currency).toEqual(token0Amount.currency); + }); + }); + + describe('when there is excess of token1', () => { + it('returns correct amountIn with simple inputs', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(1, 2); + const token0Amount = parseAmount('5', token0); + const token1Amount = parseAmount('20', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token1Amount, + token0Amount + ); + + expect(amountIn.quotient.toString()).toEqual('14000000000000000000'); + expect(amountIn.currency).toEqual(token1Amount.currency); + }); + + it('returns correct amountIn when token0 has more decimal places', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(1, 2); + const token1SixDecimals = new Token( + 1, + ADDRESS_ZERO, + 6, + 'TEST1', + 'Test Token 1' + ); + const token0Amount = parseAmount('5', token0); + const token1Amount = parseAmount('20000000000000', token1SixDecimals); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token1Amount, + token0Amount + ); + + expect(amountIn.quotient.toString()).toEqual('14000000000000000000'); + expect(amountIn.currency).toEqual(token1Amount.currency); + }); + + it('returns correct amountIn when token1 has more decimal places', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(1, 2); + const token0SixDecimals = new Token( + 1, + ADDRESS_ZERO, + 6, + 'TEST1', + 'Test Token 1' + ); + const token0Amount = parseAmount('5000000000000', token0SixDecimals); + const token1Amount = parseAmount('20', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token1Amount, + token0Amount + ); + + expect(amountIn.quotient.toString()).toEqual('14000000000000000000'); + expect(amountIn.currency).toEqual(token1Amount.currency); + }); + + it('returns correct amountIn with price greater than 1', () => { + const optimalRatio = new Fraction(1, 1); + const price = new Fraction(2, 1); + const token0Amount = parseAmount('5', token0); + const token1Amount = parseAmount('20', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token1Amount, + token0Amount + ); + + expect(amountIn.quotient.toString()).toEqual('5000000000000000000'); + expect(amountIn.currency).toEqual(token1Amount.currency); + }); + + it('returns correct amountIn when price is less than 1', () => { + const optimalRatio = new Fraction(1, 2); + const price = new Fraction(1, 2); + const token0Amount = parseAmount('5', token0); + const token1Amount = parseAmount('20', token1); + + const amountIn = calculateRatioAmountIn( + optimalRatio, + price, + token1Amount, + token0Amount + ); + + expect(amountIn.quotient.toString()).toEqual('14000000000000000000'); + expect(amountIn.currency).toEqual(token1Amount.currency); + }); + }); +}); diff --git a/test/unit/test-util/mock-data.ts b/test/unit/test-util/mock-data.ts index 35bf5c4c9..26484b654 100644 --- a/test/unit/test-util/mock-data.ts +++ b/test/unit/test-util/mock-data.ts @@ -73,6 +73,15 @@ export const USDC_DAI_MEDIUM = new Pool( 8, 0 ); +export const USDC_USDT_MEDIUM = new Pool( + USDC, + USDT, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(1, 1), + 8, + 0 +); + export const DAI_USDT_LOW = new Pool( DAI, USDT,