diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index 7808ad9718..b689063017 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -117,6 +117,11 @@ export class Ethereum extends EthereumBase implements Ethereumish { return this._metricsLogInterval; } + // in place for mocking + public get provider() { + return super.provider; + } + /** * Automatically update the prevailing gas price on the network. * diff --git a/src/connectors/uniswap/uniswap.config.ts b/src/connectors/uniswap/uniswap.config.ts index e3b5d104ba..a5a4ca4ba0 100644 --- a/src/connectors/uniswap/uniswap.config.ts +++ b/src/connectors/uniswap/uniswap.config.ts @@ -11,6 +11,9 @@ export namespace UniswapConfig { tradingTypes: (type: string) => Array; chainType: string; availableNetworks: Array; + useRouter?: boolean; + feeTier?: string; + quoterContractAddress: (network: string) => string; } export const config: NetworkConfig = { @@ -56,5 +59,11 @@ export namespace UniswapConfig { ), }, ], + useRouter: ConfigManagerV2.getInstance().get(`uniswap.useRouter`), + feeTier: ConfigManagerV2.getInstance().get(`uniswap.feeTier`), + quoterContractAddress: (network: string) => + ConfigManagerV2.getInstance().get( + `uniswap.contractAddresses.${network}.uniswapV3QuoterV2ContractAddress` + ), }; } diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index a016f2ef90..873cdc0469 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -8,7 +8,17 @@ import { } from '@ethersproject/contracts'; import { AlphaRouter } from '@uniswap/smart-order-router'; import { Trade, SwapRouter } from '@uniswap/router-sdk'; -import { MethodParameters } from '@uniswap/v3-sdk'; +import { + FeeAmount, + MethodParameters, + Pool, + SwapQuoter, + Trade as UniswapV3Trade, + Route, + FACTORY_ADDRESS, +} from '@uniswap/v3-sdk'; +import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'; +import { abi as IUniswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Factory.sol/IUniswapV3Factory.json'; import { Token, CurrencyAmount, @@ -16,7 +26,14 @@ import { TradeType, Currency, } from '@uniswap/sdk-core'; -import { BigNumber, Transaction, Wallet } from 'ethers'; +import { + BigNumber, + Transaction, + Wallet, + Contract, + utils, + constants, +} from 'ethers'; import { logger } from '../../services/logger'; import { percentRegexp } from '../../services/config-manager-v2'; import { Ethereum } from '../../chains/ethereum/ethereum'; @@ -35,6 +52,9 @@ export class Uniswap implements Uniswapish { private chainId; private tokenList: Record = {}; private _ready: boolean = false; + private readonly _useRouter: boolean; + private readonly _feeTier: FeeAmount; + private readonly _quoterContractAddress: string; private constructor(chain: string, network: string) { const config = UniswapConfig.config; @@ -53,6 +73,20 @@ export class Uniswap implements Uniswapish { this._routerAbi = routerAbi.abi; this._gasLimitEstimate = UniswapConfig.config.gasLimitEstimate; this._router = config.uniswapV3SmartOrderRouterAddress(network); + + if (config.useRouter === false && config.feeTier == null) { + throw new Error('Must specify fee tier if not using router'); + } + if (config.useRouter === false && config.quoterContractAddress == null) { + throw new Error( + 'Must specify quoter contract address if not using router' + ); + } + this._useRouter = config.useRouter ?? true; + this._feeTier = config.feeTier + ? FeeAmount[config.feeTier as keyof typeof FeeAmount] + : FeeAmount.MEDIUM; + this._quoterContractAddress = config.quoterContractAddress(network); } public static getInstance(chain: string, network: string): Uniswap { @@ -181,30 +215,61 @@ export class Uniswap implements Uniswapish { `Fetching trade data for ${baseToken.address}-${quoteToken.address}.` ); - const route = await this._alphaRouter.route( - nativeTokenAmount, - quoteToken, - TradeType.EXACT_INPUT, - undefined, - { - maxSwapsPerPath: this.maximumHops, - } - ); + if (this._useRouter) { + const route = await this._alphaRouter.route( + nativeTokenAmount, + quoteToken, + TradeType.EXACT_INPUT, + undefined, + { + maxSwapsPerPath: this.maximumHops, + } + ); - if (!route) { - throw new UniswapishPriceError( - `priceSwapIn: no trade pair found for ${baseToken} to ${quoteToken}.` + if (!route) { + throw new UniswapishPriceError( + `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.` + ); + } + logger.info( + `Best trade for ${baseToken.address}-${quoteToken.address}: ` + + `${route.trade.executionPrice.toFixed(6)}` + + `${baseToken.symbol}.` + ); + const expectedAmount = route.trade.minimumAmountOut( + this.getAllowedSlippage(allowedSlippage) ); + return { trade: route.trade, expectedAmount }; + } else { + const pool = await this.getPool(baseToken, quoteToken, this._feeTier); + if (!pool) { + throw new UniswapishPriceError( + `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.` + ); + } + const swapRoute = new Route([pool], baseToken, quoteToken); + const quotedAmount = await this.getQuote( + swapRoute, + quoteToken, + nativeTokenAmount, + TradeType.EXACT_INPUT + ); + const trade = UniswapV3Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: nativeTokenAmount, + outputAmount: quotedAmount, + tradeType: TradeType.EXACT_INPUT, + }); + logger.info( + `Best trade for ${baseToken.address}-${quoteToken.address}: ` + + `${trade.executionPrice.toFixed(6)}` + + `${baseToken.symbol}.` + ); + const expectedAmount = trade.minimumAmountOut( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade, expectedAmount }; } - logger.info( - `Best trade for ${baseToken.address}-${quoteToken.address}: ` + - `${route.trade.executionPrice.toFixed(6)}` + - `${baseToken.symbol}.` - ); - const expectedAmount = route.trade.minimumAmountOut( - this.getAllowedSlippage(allowedSlippage) - ); - return { trade: route.trade, expectedAmount }; } /** @@ -228,30 +293,62 @@ export class Uniswap implements Uniswapish { logger.info( `Fetching pair data for ${quoteToken.address}-${baseToken.address}.` ); - const route = await this._alphaRouter.route( - nativeTokenAmount, - quoteToken, - TradeType.EXACT_OUTPUT, - undefined, - { - maxSwapsPerPath: this.maximumHops, + + if (this._useRouter) { + const route = await this._alphaRouter.route( + nativeTokenAmount, + quoteToken, + TradeType.EXACT_OUTPUT, + undefined, + { + maxSwapsPerPath: this.maximumHops, + } + ); + if (!route) { + throw new UniswapishPriceError( + `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.` + ); } - ); - if (!route) { - throw new UniswapishPriceError( - `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.` + logger.info( + `Best trade for ${quoteToken.address}-${baseToken.address}: ` + + `${route.trade.executionPrice.invert().toFixed(6)} ` + + `${baseToken.symbol}.` ); - } - logger.info( - `Best trade for ${quoteToken.address}-${baseToken.address}: ` + - `${route.trade.executionPrice.invert().toFixed(6)} ` + - `${baseToken.symbol}.` - ); - const expectedAmount = route.trade.maximumAmountIn( - this.getAllowedSlippage(allowedSlippage) - ); - return { trade: route.trade, expectedAmount }; + const expectedAmount = route.trade.maximumAmountIn( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade: route.trade, expectedAmount }; + } else { + const pool = await this.getPool(quoteToken, baseToken, this._feeTier); + if (!pool) { + throw new UniswapishPriceError( + `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.` + ); + } + const swapRoute = new Route([pool], quoteToken, baseToken); + const quotedAmount = await this.getQuote( + swapRoute, + quoteToken, + nativeTokenAmount, + TradeType.EXACT_OUTPUT + ); + const trade = UniswapV3Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: quotedAmount, + outputAmount: nativeTokenAmount, + tradeType: TradeType.EXACT_OUTPUT, + }); + logger.info( + `Best trade for ${baseToken.address}-${quoteToken.address}: ` + + `${trade.executionPrice.invert().toFixed(6)}` + + `${baseToken.symbol}.` + ); + const expectedAmount = trade.maximumAmountIn( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade, expectedAmount }; + } } /** @@ -308,7 +405,7 @@ export class Uniswap implements Uniswapish { } else { tx = await wallet.sendTransaction({ data: methodParameters.calldata, - to: this.router, + to: uniswapRouter, gasPrice: (gasPrice * 1e9).toFixed(0), gasLimit: gasLimit.toFixed(0), value: methodParameters.value, @@ -320,4 +417,74 @@ export class Uniswap implements Uniswapish { } ); } + + private async getPool( + tokenA: Token, + tokenB: Token, + feeTier: FeeAmount + ): Promise { + const uniswapFactory = new Contract( + FACTORY_ADDRESS, + IUniswapV3FactoryABI, + this.chain.provider + ); + // Use Uniswap V3 factory to get pool address instead of `Pool.getAddress` to check if pool exists. + const poolAddress = await uniswapFactory.getPool( + tokenA.address, + tokenB.address, + feeTier + ); + if (poolAddress === constants.AddressZero) { + return null; + } + const poolContract = new Contract( + poolAddress, + IUniswapV3PoolABI, + this.chain.provider + ); + + const [liquidity, slot0] = await Promise.all([ + poolContract.liquidity(), + poolContract.slot0(), + ]); + const [sqrtPriceX96, tick] = slot0; + + const pool = new Pool( + tokenA, + tokenB, + this._feeTier, + sqrtPriceX96, + liquidity, + tick + ); + + return pool; + } + + private async getQuote( + swapRoute: Route, + quoteToken: Token, + amount: CurrencyAmount, + tradeType: TradeType + ) { + const { calldata } = await SwapQuoter.quoteCallParameters( + swapRoute, + amount, + tradeType, + { useQuoterV2: true } + ); + const quoteCallReturnData = await this.chain.provider.call({ + to: this._quoterContractAddress, + data: calldata, + }); + const quoteTokenRawAmount = utils.defaultAbiCoder.decode( + ['uint256'], + quoteCallReturnData + ); + const qouteTokenAmount = CurrencyAmount.fromRawAmount( + quoteToken, + quoteTokenRawAmount.toString() + ); + return qouteTokenAmount; + } } diff --git a/src/services/common-interfaces.ts b/src/services/common-interfaces.ts index 4d57854d3b..7f4b2559b9 100644 --- a/src/services/common-interfaces.ts +++ b/src/services/common-interfaces.ts @@ -148,7 +148,6 @@ export type UniswapishTrade = | TradeQuickswap | TradeTraderjoe | SushiswapTrade - | UniswapV3Trade | TradeUniswap | TradeDefikingdoms | DefiraTrade diff --git a/src/services/schema/uniswap-schema.json b/src/services/schema/uniswap-schema.json index 91710caf14..3585d376b4 100644 --- a/src/services/schema/uniswap-schema.json +++ b/src/services/schema/uniswap-schema.json @@ -6,6 +6,10 @@ "gasLimitEstimate": { "type": "integer" }, "ttl": { "type": "integer" }, "maximumHops": { "type": "integer" }, + "useRouter": { "type": "boolean" }, + "feeTier": { + "enum": ["LOWEST", "LOW", "MEDIUM", "HIGH"] + }, "contractAddresses": { "type": "object", "patternProperties": { @@ -13,7 +17,8 @@ "type": "object", "properties": { "uniswapV3SmartOrderRouterAddress": { "type": "string" }, - "uniswapV3NftManagerAddress": { "type": "string" } + "uniswapV3NftManagerAddress": { "type": "string" }, + "uniswapV3QuoterV2ContractAddress": { "type": "string" } }, "required": [ "uniswapV3SmartOrderRouterAddress", diff --git a/src/templates/uniswap.yml b/src/templates/uniswap.yml index f4001280d2..c1f00b1487 100644 --- a/src/templates/uniswap.yml +++ b/src/templates/uniswap.yml @@ -13,19 +13,34 @@ ttl: 600 # Note: More hops will increase latency of the algorithm. maximumHops: 4 +# Use Uniswap Router or Quoter for quoting prices. +# true - use Uniswap Router. +# false - use Uniswap Quoter. +useRouter: true + +# Fee tier to use for the Uniswap Quoter. +# Required if `useRouter` is false. +# Available options: 'LOWEST', 'LOW', 'MEDIUM', 'HIGH'. +feeTier: 'MEDIUM' + contractAddresses: mainnet: uniswapV3SmartOrderRouterAddress: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' uniswapV3NftManagerAddress: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' + uniswapV3QuoterV2ContractAddress: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' goerli: uniswapV3SmartOrderRouterAddress: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' uniswapV3NftManagerAddress: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' + uniswapV3QuoterV2ContractAddress: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' arbitrum_one: uniswapV3SmartOrderRouterAddress: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' uniswapV3NftManagerAddress: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' + uniswapV3QuoterV2ContractAddress: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' optimism: uniswapV3SmartOrderRouterAddress: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' uniswapV3NftManagerAddress: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' + uniswapV3QuoterV2ContractAddress: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' mumbai: uniswapV3SmartOrderRouterAddress: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' uniswapV3NftManagerAddress: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' + uniswapV3QuoterV2ContractAddress: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' diff --git a/test/connectors/uniswap/uniswap.test.ts b/test/connectors/uniswap/uniswap.test.ts index 3b666a653d..ff81685ad6 100644 --- a/test/connectors/uniswap/uniswap.test.ts +++ b/test/connectors/uniswap/uniswap.test.ts @@ -1,16 +1,26 @@ jest.useFakeTimers(); +const { MockProvider } = require('mock-ethers-provider'); import { Uniswap } from '../../../src/connectors/uniswap/uniswap'; import { patch, unpatch } from '../../services/patch'; import { UniswapishPriceError } from '../../../src/services/error-handler'; import { CurrencyAmount, Percent, TradeType, Token } from '@uniswap/sdk-core'; import { Pair, Route } from '@uniswap/v2-sdk'; import { Trade } from '@uniswap/router-sdk'; -import { BigNumber, utils } from 'ethers'; +import { BigNumber, constants, utils } from 'ethers'; import { Ethereum } from '../../../src/chains/ethereum/ethereum'; import { patchEVMNonceManager } from '../../evm.nonce.mock'; +import { UniswapConfig } from '../../../src/connectors/uniswap/uniswap.config'; +import { + FACTORY_ADDRESS, + TickMath, + encodeSqrtRatioX96, + Pool as UniswapV3Pool, + FeeAmount, +} from '@uniswap/v3-sdk'; let ethereum: Ethereum; let uniswap: Uniswap; +let mockProvider: typeof MockProvider; const WETH = new Token( 3, @@ -26,13 +36,23 @@ const DAI = new Token( 'DAI' ); +const DAI_WETH_POOL_ADDRESS = '0xBEff876AC507446457C2A6bDA9F7021A97A8547f'; +const POOL_SQRT_RATIO_START = encodeSqrtRatioX96(100e6, 100e18); +const POOL_TICK_CURRENT = TickMath.getTickAtSqrtRatio(POOL_SQRT_RATIO_START); +const POOL_LIQUIDITY = 0; +const DAI_WETH_POOL = new UniswapV3Pool( + WETH, + DAI, + FeeAmount.MEDIUM, + POOL_SQRT_RATIO_START, + POOL_LIQUIDITY, + POOL_TICK_CURRENT +); + beforeAll(async () => { ethereum = Ethereum.getInstance('goerli'); patchEVMNonceManager(ethereum.nonceManager); await ethereum.init(); - - uniswap = Uniswap.getInstance('ethereum', 'goerli'); - await uniswap.init(); }); beforeEach(() => { @@ -94,47 +114,196 @@ const patchTrade = (_key: string, error?: Error) => { }); }; +const patchMockProvider = () => { + mockProvider.setMockContract( + FACTORY_ADDRESS, + require('@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json') + .abi + ); + mockProvider.stub(FACTORY_ADDRESS, 'getPool', DAI_WETH_POOL_ADDRESS); + + mockProvider.setMockContract( + UniswapConfig.config.quoterContractAddress('goerli'), + require('@uniswap/swap-router-contracts/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json') + .abi + ); + mockProvider.stub( + UniswapConfig.config.quoterContractAddress('goerli'), + 'quoteExactInputSingle', + /* amountOut */ 1, + /* sqrtPriceX96After */ 0, + /* initializedTicksCrossed */ 0, + /* gasEstimate */ 0 + ); + mockProvider.stub( + UniswapConfig.config.quoterContractAddress('goerli'), + 'quoteExactOutputSingle', + /* amountIn */ 1, + /* sqrtPriceX96After */ 0, + /* initializedTicksCrossed */ 0, + /* gasEstimate */ 0 + ); + + mockProvider.setMockContract( + DAI_WETH_POOL_ADDRESS, + require('@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json') + .abi + ); + mockProvider.stub( + DAI_WETH_POOL_ADDRESS, + 'slot0', + DAI_WETH_POOL.sqrtRatioX96.toString(), + DAI_WETH_POOL.tickCurrent, + /* observationIndex */ 0, + /* observationCardinality */ 1, + /* observationCardinalityNext */ 1, + /* feeProtocol */ 0, + /* unlocked */ true + ); + mockProvider.stub(DAI_WETH_POOL_ADDRESS, 'liquidity', 0); + patch(ethereum, 'provider', () => { + return mockProvider; + }); +}; + +const patchGetPool = (address: string | null) => { + mockProvider.setMockContract( + FACTORY_ADDRESS, + require('@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json') + .abi + ); + mockProvider.stub(FACTORY_ADDRESS, 'getPool', address); +}; + +const useRouter = async () => { + const config = UniswapConfig.config; + config.useRouter = true; + + patch(Uniswap, '_instances', () => ({})); + uniswap = Uniswap.getInstance('ethereum', 'goerli'); + await uniswap.init(); +}; + +const useQouter = async () => { + const config = UniswapConfig.config; + config.useRouter = false; + config.feeTier = 'MEDIUM'; + + patch(Uniswap, '_instances', () => ({})); + uniswap = Uniswap.getInstance('ethereum', 'goerli'); + await uniswap.init(); + + mockProvider = new MockProvider(); + patchMockProvider(); +}; + describe('verify Uniswap estimateSellTrade', () => { - it('Should return an ExpectedTrade when available', async () => { - patchTrade('bestTradeExactIn'); + describe('when using router', () => { + beforeAll(async () => { + await useRouter(); + }); - const expectedTrade = await uniswap.estimateSellTrade( - WETH, - DAI, - BigNumber.from(1) - ); - expect(expectedTrade).toHaveProperty('trade'); - expect(expectedTrade).toHaveProperty('expectedAmount'); + it('Should return an ExpectedTrade when available', async () => { + patchTrade('bestTradeExactIn'); + + const expectedTrade = await uniswap.estimateSellTrade( + WETH, + DAI, + BigNumber.from(1) + ); + expect(expectedTrade).toHaveProperty('trade'); + expect(expectedTrade).toHaveProperty('expectedAmount'); + }); + + it('Should throw an error if no pair is available', async () => { + patchTrade('bestTradeExactIn', new Error('error getting trade')); + + await expect(async () => { + await uniswap.estimateSellTrade(WETH, DAI, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); }); - it('Should throw an error if no pair is available', async () => { - patchTrade('bestTradeExactIn', new Error('error getting trade')); + describe('when using qouter', () => { + beforeEach(async () => { + await useQouter(); + }); + + it('Should return an ExpectedTrade when available', async () => { + patchGetPool(DAI_WETH_POOL_ADDRESS); + + const expectedTrade = await uniswap.estimateSellTrade( + WETH, + DAI, + BigNumber.from(1) + ); + + expect(expectedTrade).toHaveProperty('trade'); + expect(expectedTrade).toHaveProperty('expectedAmount'); + }); + + it('Should throw an error if no pair is available', async () => { + patchGetPool(constants.AddressZero); - await expect(async () => { - await uniswap.estimateSellTrade(WETH, DAI, BigNumber.from(1)); - }).rejects.toThrow(UniswapishPriceError); + await expect(async () => { + await uniswap.estimateSellTrade(WETH, DAI, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); }); }); describe('verify Uniswap estimateBuyTrade', () => { - it('Should return an ExpectedTrade when available', async () => { - patchTrade('bestTradeExactOut'); + describe('when using router', () => { + beforeAll(async () => { + await useRouter(); + }); - const expectedTrade = await uniswap.estimateBuyTrade( - WETH, - DAI, - BigNumber.from(1) - ); - expect(expectedTrade).toHaveProperty('trade'); - expect(expectedTrade).toHaveProperty('expectedAmount'); + it('Should return an ExpectedTrade when available', async () => { + patchTrade('bestTradeExactOut'); + + const expectedTrade = await uniswap.estimateBuyTrade( + WETH, + DAI, + BigNumber.from(1) + ); + expect(expectedTrade).toHaveProperty('trade'); + expect(expectedTrade).toHaveProperty('expectedAmount'); + }); + + it('Should return an error if no pair is available', async () => { + patchTrade('bestTradeExactOut', new Error('error getting trade')); + + await expect(async () => { + await uniswap.estimateBuyTrade(WETH, DAI, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); }); - it('Should return an error if no pair is available', async () => { - patchTrade('bestTradeExactOut', new Error('error getting trade')); + describe('when using qouter', () => { + beforeEach(async () => { + await useQouter(); + }); + + it('Should return an ExpectedTrade when available', async () => { + patchGetPool(DAI_WETH_POOL_ADDRESS); + + const expectedTrade = await uniswap.estimateBuyTrade( + WETH, + DAI, + BigNumber.from(1) + ); + + expect(expectedTrade).toHaveProperty('trade'); + expect(expectedTrade).toHaveProperty('expectedAmount'); + }); + + it('Should throw an error if no pair is available', async () => { + patchGetPool(constants.AddressZero); - await expect(async () => { - await uniswap.estimateBuyTrade(WETH, DAI, BigNumber.from(1)); - }).rejects.toThrow(UniswapishPriceError); + await expect(async () => { + await uniswap.estimateBuyTrade(WETH, DAI, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); }); });