From 8c9ff06cedb9dc5b7cfc3ec71b0de8edfb19f5fa Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 4 Aug 2022 07:35:29 -0700 Subject: [PATCH] Squashed commit of the following: commit 807b37b63c85fbd28f7d77331d8d63b721231459 Author: Eric Zhong Date: Tue Aug 2 21:51:31 2022 -0700 Add test for mixedRoute priceImpact commit a4b22f3b6562cb7cf7e56d30ffffef6bd7f41ce6 Merge: 12ba497 3f996ce Author: Eric Zhong Date: Tue Aug 2 21:10:01 2022 -0700 Merge branch 'main' into eric/support-mixed-route-paths commit 12ba497eb9bb9d983eba40e1f2714b8b7044d0b6 Author: Eric Zhong Date: Tue Aug 2 21:06:21 2022 -0700 Export utils.index commit 4e3f85a0ff7c36c38ac35b7f32ee65741d047f6d Author: Eric Zhong Date: Sat Jul 30 19:15:53 2022 -0700 Revisions commit bca28a9fc8977046375cbbe464a2d86f90cbbbfd Author: Eric Zhong Date: Mon Jul 25 17:42:26 2022 -0400 Revisions: clean up dividingMixedRoutes util, unused vars in swapRouter + comments commit c78d76640523b8bac3bd0db09be114433348c6cb Author: Eric Zhong Date: Thu Jul 21 18:31:18 2022 -0400 Untrack tarball commit 06e9d75ddc85cc3924a45dacf2329773754d9d49 Author: Eric Zhong Date: Thu Jul 21 18:29:13 2022 -0400 Unskip test commit ac9f51fe393776a316ba4392f94bc87bf869d443 Author: Eric Zhong Date: Thu Jul 21 18:29:04 2022 -0400 Revert yarn.lock to main commit 68387dbc0b7def6c35c48006e3332138c55c58e2 Author: Eric Zhong Date: Thu Jul 21 14:07:41 2022 -0400 Resolve TODOS commit bf3f28554cebb522fb901d892f57a0dbb3433341 Author: Eric Zhong Date: Thu Jul 21 14:05:21 2022 -0400 Separate some helpers into utils/index.ts commit 32d6a4c5db64e71d948ea11882388eb8990162d2 Author: Eric Zhong Date: Thu Jul 21 13:32:07 2022 -0400 Add notice comment commit 718b21858c1bc72a2bf3707461bc70722e784428 Author: Eric Zhong Date: Thu Jul 21 13:30:16 2022 -0400 Remove exactIn from mixedRoute/trade commit b696e9e4fb585effbe04205adc6e938526079297 Author: Eric Zhong Date: Thu Jul 21 12:39:12 2022 -0400 Add tests for tradeType validation commit 17c964df2f24d06fa053af2ebccefec533142c12 Author: Eric Zhong Date: Thu Jul 21 12:27:01 2022 -0400 Add mixedRoute to all tests in swapRouter.ts commit d609bebbc64e7db91c863b3c7ca95dc64fcd601e Author: Eric Zhong Date: Thu Jul 21 12:05:15 2022 -0400 Update invariant messages for tradeType validation for mixedRoutes commit 7cfd3391e54cbf099f94f4ea6c5719552c4836f5 Author: Eric Zhong Date: Thu Jul 21 12:04:05 2022 -0400 Fix bug where routerMustCustody was not true for unwrap weth commit 3a64dc92860ad4bcbb7379929b8f4e2517468153 Author: Eric Zhong Date: Thu Jul 21 11:10:16 2022 -0400 Add mixedRoute tests for worstExecutionPrices commit b5de080edd9f30712e15f2b0cd0a971781c1ba93 Author: Eric Zhong Date: Wed Jul 20 18:19:26 2022 -0400 Add rest of tests involving mixedRoutes in generic trade test commit 27717dcb89194bf50c39b15c0c60dab943ffc460 Author: Eric Zhong Date: Wed Jul 20 17:56:24 2022 -0400 Add a bunch of tests for Trades with MixedRouteSDK commit 967a96b65b6bfd6eee56f3ce2bccce9ba264ba3d Author: Eric Zhong Date: Wed Jul 20 16:58:43 2022 -0400 remove comment commit 87b1e7a341b8990cad4cacf6c4ce81b502636f88 Author: Eric Zhong Date: Wed Jul 20 15:47:24 2022 -0400 Revert changes to entities/route to implement IRoute commit e2ba0612699dc355d2c252091480ae40189c0138 Author: Eric Zhong Date: Wed Jul 20 15:06:46 2022 -0400 MixedRouteSDK implements IRoute commit 05f38d398d86bbae91677e6db4f22b080702f1c7 Author: Eric Zhong Date: Wed Jul 20 14:50:11 2022 -0400 fix bug in encodeSwaps that would reassign types commit 014a2a37c229ad1080034e44f22451c0ee2b46aa Author: Eric Zhong Date: Wed Jul 20 14:13:54 2022 -0400 Fix single hop in encoding mixedroutes commit 66346b0beacdc0ea6dc5e4369d04a7be9c34512c Author: Eric Zhong Date: Wed Jul 20 13:40:59 2022 -0400 Fix section MixedRoute construction again commit 5aed2f7df76be4c20b8bb73b556db54c98a4a2a3 Author: Eric Zhong Date: Fri Jul 15 16:20:18 2022 -0400 Figure out mid price calculations commit ac39c9df321179488d8ab4eba1d57d01964b2715 Author: Eric Zhong Date: Fri Jul 15 11:40:39 2022 -0400 Add some more complex mixedRoute tests commit fa59131e2c67ab8abbadf5ad2a680b57fb7de765 Author: Eric Zhong Date: Fri Jul 15 11:27:09 2022 -0400 Add more tests to add more coverage for mixedRoute creation commit f70d9da60001494342575f56c29cfc3ad4b8a6aa Author: Eric Zhong Date: Fri Jul 15 11:14:00 2022 -0400 Fix bug in building mixedRoutes from sections commit 29dee1269b8553820728c74c11dfcbaffd8586bf Author: Eric Zhong Date: Thu Jul 14 12:09:06 2022 -0400 Import BestTradeOptions from v3-sdk commit dcda2c9c2642ed89b8fc4a00cec932c760c91a66 Author: Eric Zhong Date: Thu Jul 14 11:55:29 2022 -0400 Fix call to encodeMixedRoutetoPath commit e10cb4ffb2eb579219bc377ff3b8337a265d59ef Author: Eric Zhong Date: Thu Jul 14 11:52:47 2022 -0400 Revisions: change mixedRouteWrapper to remove tokenPath and remove param for encodeMixedRouteToPath commit 02e24e8cc5f8bb3ce2dd3ba2d0f174738af9db42 Author: Eric Zhong Date: Mon Jul 11 12:47:53 2022 -0400 Self revisions, change naming, doc comments, etc. commit c096c4e7649a9b8f083af0b8483c2d99c4419aab Author: Eric Zhong Date: Thu Jun 30 17:15:57 2022 -0400 Support single hop in MixedRoute and verify that calldata encoding is correct commit cfc6fef155b3a5b74ade02a1c3535e9f55b51070 Author: Eric Zhong Date: Thu Jun 30 16:29:26 2022 -0400 Add all tests for MixedRouteTrade commit 98513b6b6049a6fc5e6abbc568d6bb2db3cd8442 Author: Eric Zhong Date: Thu Jun 30 15:34:59 2022 -0400 Add tests for MixedRouteTrade commit 1b9650287f442b2e053c5737bedda57d33fb74a8 Author: Eric Zhong Date: Thu Jun 30 15:34:49 2022 -0400 add missed pair-specific logic in bestTradeExactIn commit 0ba579590c82f24d2e5b6d4f8344aa0f1605aa98 Author: Eric Zhong Date: Thu Jun 30 14:56:29 2022 -0400 Update swap router commit 1b19d84ac930d381f8b653a87a0e2994f3231e28 Author: Eric Zhong Date: Thu Jun 30 14:56:15 2022 -0400 simplify mixedRoute/trade.ts commit dd025a8f94e128f446503819518e51090d34e794 Author: Eric Zhong Date: Thu Jun 30 11:49:19 2022 -0400 fix input output issue with new sections commit f89b9af478ed47161c06a3f74d2f4f5d57b9c13b Author: Eric Zhong Date: Wed Jun 29 18:45:45 2022 -0400 section batching is broken rn need to fix mixedRouteSDK creation commit 502676ff92b7457fe8a8f34e1a85ef4dd521236b Author: Eric Zhong Date: Wed Jun 29 18:04:51 2022 -0400 Add batching and fix typo in encodeMixedRouteToPath to pass basic test commit 74308387930d11f1215b1496295a097010c600b2 Author: Eric Zhong Date: Wed Jun 29 17:34:57 2022 -0400 add consecutive chunking logic for encoding mixedRoute commit ca4af8476f00d40ee26b0abef0f6471f992f2705 Author: Eric Zhong Date: Wed Jun 29 17:16:12 2022 -0400 fix amountOut condition for mixedRoute commit d0317ce19f7c9b9c3f88f2ba694a64ef1f747a9f Author: Eric Zhong Date: Wed Jun 29 17:09:38 2022 -0400 clean up commit 59632f92bc32f72b4a74b2f21f4de01502d9f95c Author: Eric Zhong Date: Wed Jun 29 17:06:25 2022 -0400 cleaning up encodeMixedRoutePaths commit 53bfdc0ab7ab89cc86be3e07341da06a5d09b3b7 Author: Eric Zhong Date: Wed Jun 29 16:50:28 2022 -0400 call data successfull! commit c498b4e70c549c76b65cb2e2c222a137e14f5fc4 Author: Eric Zhong Date: Wed Jun 29 16:22:14 2022 -0400 add basic semi broken calldata generation for mixedroute trades commit 59936975d72119f68e80c9f82de83d1826337ec6 Author: Eric Zhong Date: Tue Jun 28 14:06:06 2022 -0400 change var assignment in fromRoute commit 54804f86347b0635c8ef85832f8bde931a65044a Author: Eric Zhong Date: Tue Jun 28 13:55:09 2022 -0400 some changes while debugging commit 3914891f8d4dfb447a523d0f0edabe91d5fe6f40 Author: Eric Zhong Date: Mon Jun 27 17:27:31 2022 -0400 clean up swapRouter.ts to add encodeSwap func for mixedRoute commit 272bcafd2a55c8ed5aae4a9faf0460d24988b554 Author: Eric Zhong Date: Mon Jun 27 16:03:16 2022 -0400 fix broken tests commit b73ab12225fdc8d15ea0ebf059d79018887fea09 Author: Eric Zhong Date: Mon Jun 27 16:01:33 2022 -0400 rescued! commit 12b96797cdaa7cdfc5199081ecba244e541f9506 Author: Eric Zhong Date: Mon Jun 27 14:40:22 2022 -0400 un add tarball commit 3185287221df43a300198a5fda881a2ce54284da Author: Eric Zhong Date: Mon Jun 27 14:39:19 2022 -0400 bump v3-sdk to 3.8.3 commit a97018cbcf1b4e81fe2f750033b1833e8fbb081a Author: Eric Zhong Date: Wed Jun 22 16:38:25 2022 -0400 pin v3-sdk at 3.8.2 commit 86bb04899e37ed3273451eca10290b690300c2fc Author: Eric Zhong Date: Wed Jun 22 14:27:10 2022 -0400 Add implementaiton for generic MixedRouteSDK class, and refactor commit a19ceaf220dad9bd2d086e401ca37f39a273494a Author: Eric Zhong Date: Wed Jun 22 12:32:57 2022 -0400 remove comment commit 7528a67bccd2a461e2af63ef7476ba5ae82c4d5f Author: Eric Zhong Date: Wed Jun 22 12:25:51 2022 -0400 remove .onlys commit 6bc0c116405928f641ad933c533b235e20827fbf Author: Eric Zhong Date: Wed Jun 22 12:22:10 2022 -0400 Add final encodePath tests for mixed route commit f81420bdab76b06febf304dad31ea24a0cb6d069 Author: Eric Zhong Date: Wed Jun 22 12:16:53 2022 -0400 add encodeMixedRoutetoPath tests commit c64946df5234ce4c33d2706d7ef080067dee3a4e Author: Eric Zhong Date: Fri Jun 17 15:41:05 2022 -0400 Add another tokenPath test commit ad06d4b304b679e9e24d347d067ccf6e85ed2ec3 Author: Eric Zhong Date: Fri Jun 17 15:35:47 2022 -0400 Clean up some comments commit 02f2f213669b3a5d3fd19006007dc8fee454347b Author: Eric Zhong Date: Fri Jun 17 15:30:51 2022 -0400 Clean up tests commit 1729c4b2c5527771ef02f19259df27728b6a52a4 Author: Eric Zhong Date: Fri Jun 17 15:26:41 2022 -0400 Add basic test for MixedRoute midPrice, math TBD commit 1a56f48b36f244b242c13c5293bf636d9f10bf48 Author: Eric Zhong Date: Fri Jun 17 15:17:24 2022 -0400 refactor tests commit 42d50069db783cb81e84615e35aae699f4a95220 Author: Eric Zhong Date: Fri Jun 17 15:08:53 2022 -0400 Add basic tests for V2 route mid prices commit f7da4fc68bac77c53777c9902f63e566c67ea730 Author: Eric Zhong Date: Fri Jun 17 14:46:47 2022 -0400 Add tests to ensure backwards compatability with v3 midprice commit a764781b4f2739d8c9c59ba18df197fee07dc9c9 Author: Eric Zhong Date: Fri Jun 17 14:42:47 2022 -0400 Initial idea for midPrice support in mixedRoute commit 8e7f66cc6380a7e581098725feda23f220525a64 Author: Eric Zhong Date: Thu Jun 16 18:11:14 2022 -0400 Add comment commit a957d6f3a14709d28213cdf401c7a46f7d520940 Author: Eric Zhong Date: Thu Jun 16 18:08:59 2022 -0400 Add basic tests for MixedRoute tokenPath creation commit 3706854f2802ab69071d67e516fe92caf4439aee Author: Eric Zhong Date: Thu Jun 16 17:51:29 2022 -0400 Add mixedRoute class and create encodeMixedRouteToPath --- src/constants.ts | 3 + src/entities/mixedRoute/route.test.ts | 473 ++++++++ src/entities/mixedRoute/route.ts | 92 ++ src/entities/mixedRoute/trade.test.ts | 1385 ++++++++++++++++++++++ src/entities/mixedRoute/trade.ts | 500 ++++++++ src/entities/protocol.ts | 1 + src/entities/route.ts | 15 + src/entities/trade.test.ts | 397 ++++++- src/entities/trade.ts | 79 +- src/index.ts | 3 + src/swapRouter.test.ts | 742 +++++++++++- src/swapRouter.ts | 214 +++- src/utils/encodeMixedRouteToPath.test.ts | 139 +++ src/utils/encodeMixedRouteToPath.ts | 42 + src/utils/index.ts | 52 + yarn.lock | 2 +- 16 files changed, 4087 insertions(+), 52 deletions(-) create mode 100644 src/entities/mixedRoute/route.test.ts create mode 100644 src/entities/mixedRoute/route.ts create mode 100644 src/entities/mixedRoute/trade.test.ts create mode 100644 src/entities/mixedRoute/trade.ts create mode 100644 src/utils/encodeMixedRouteToPath.test.ts create mode 100644 src/utils/encodeMixedRouteToPath.ts create mode 100644 src/utils/index.ts diff --git a/src/constants.ts b/src/constants.ts index 1ea4ff5..33f277d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,3 +5,6 @@ export const ADDRESS_THIS = '0x0000000000000000000000000000000000000002' export const ZERO = JSBI.BigInt(0) export const ONE = JSBI.BigInt(1) + +// = 1 << 23 or 100000000000000000000000 +export const V2_FEE_PATH_PLACEHOLDER = 8388608 diff --git a/src/entities/mixedRoute/route.test.ts b/src/entities/mixedRoute/route.test.ts new file mode 100644 index 0000000..2cb9f0d --- /dev/null +++ b/src/entities/mixedRoute/route.test.ts @@ -0,0 +1,473 @@ +import { Ether, Token, WETH9, CurrencyAmount, Currency } from '@uniswap/sdk-core' +import { Route as V3RouteSDK, Pool, FeeAmount, TickMath, encodeSqrtRatioX96 } from '@uniswap/v3-sdk' +import { MixedRoute, RouteV3 } from '../route' +import { Protocol } from '../protocol' +import { Route as V2RouteSDK, Pair } from '@uniswap/v2-sdk' +import { MixedRouteSDK } from './route' +import { partitionMixedRouteByProtocol } from '../../utils' + +describe('MixedRoute', () => { + const ETHER = Ether.onChain(1) + const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0') + const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1') + const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2') + const token3 = new Token(1, '0x0000000000000000000000000000000000000004', 18, 't3') + const weth = WETH9[1] + + const pool_0_1 = new Pool(token0, token1, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_0_weth = new Pool(token0, weth, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_1_weth = new Pool(token1, weth, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_2_weth = new Pool(token2, weth, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_2_3 = new Pool(token2, token3, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + /// @dev copied from v2-sdk route.test.ts + const pair_0_1 = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(token1, '200')) + const pair_0_weth = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(weth, '100')) + const pair_1_weth = new Pair(CurrencyAmount.fromRawAmount(token1, '175'), CurrencyAmount.fromRawAmount(weth, '100')) + const pair_weth_2 = new Pair(CurrencyAmount.fromRawAmount(weth, '200'), CurrencyAmount.fromRawAmount(token2, '150')) + const pair_2_3 = new Pair(CurrencyAmount.fromRawAmount(token2, '100'), CurrencyAmount.fromRawAmount(token3, '200')) + + describe('path', () => { + it('wraps pure v3 route object and successfully constructs a path from the tokens', () => { + /// @dev since the MixedRoute sdk object lives here in router-sdk we don't need to wrap it + const routeOriginal = new MixedRouteSDK([pool_0_1], token0, token1) + const route = new MixedRoute(routeOriginal) + expect(route.pools).toEqual([pool_0_1]) + expect(route.path).toEqual([token0, token1]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token1) + expect(route.chainId).toEqual(1) + }) + + it('wraps pure v2 route object and successfully constructs a path from the tokens', () => { + const route = new MixedRouteSDK([pair_0_1], token0, token1) + expect(route.pools).toEqual([pair_0_1]) + expect(route.path).toEqual([token0, token1]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token1) + expect(route.chainId).toEqual(1) + }) + + it('wraps mixed route object and successfully constructs a path from the tokens', () => { + const route = new MixedRouteSDK([pool_0_1, pair_1_weth], token0, weth) + expect(route.pools).toEqual([pool_0_1, pair_1_weth]) + expect(route.path).toEqual([token0, token1, weth]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(weth) + expect(route.chainId).toEqual(1) + }) + + it('wraps complex mixed route object and successfully constructs a path from the tokens', () => { + const route = new MixedRouteSDK([pool_0_1, pair_1_weth, pair_weth_2], token0, token2) + expect(route.pools).toEqual([pool_0_1, pair_1_weth, pair_weth_2]) + expect(route.path).toEqual([token0, token1, weth, token2]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token2) + expect(route.chainId).toEqual(1) + }) + + it('wraps complex mixed route object with multihop V3 in the beginning and constructs a path', () => { + const route = new MixedRouteSDK([pool_0_1, pool_1_weth, pair_weth_2], token0, token2) + expect(route.pools).toEqual([pool_0_1, pool_1_weth, pair_weth_2]) + expect(route.path).toEqual([token0, token1, weth, token2]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token2) + expect(route.chainId).toEqual(1) + }) + + it('wraps complex mixed route object with multihop V2 in the beginning and constructs a path', () => { + const route = new MixedRouteSDK([pair_0_1, pair_1_weth, pool_2_weth], token0, token2) + expect(route.pools).toEqual([pair_0_1, pair_1_weth, pool_2_weth]) + expect(route.path).toEqual([token0, token1, weth, token2]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token2) + expect(route.chainId).toEqual(1) + }) + + it('wraps complex mixed route object with consecutive V3 in the middle and constructs a path', () => { + const route = new MixedRouteSDK([pair_0_1, pool_1_weth, pool_2_weth, pair_2_3], token0, token3) + expect(route.pools).toEqual([pair_0_1, pool_1_weth, pool_2_weth, pair_2_3]) + expect(route.path).toEqual([token0, token1, weth, token2, token3]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token3) + expect(route.chainId).toEqual(1) + }) + + it('wraps complex mixed route object with consecutive V2 in the middle and constructs a path', () => { + const route = new MixedRouteSDK([pool_0_1, pair_1_weth, pair_weth_2, pool_2_3], token0, token3) + expect(route.pools).toEqual([pool_0_1, pair_1_weth, pair_weth_2, pool_2_3]) + expect(route.path).toEqual([token0, token1, weth, token2, token3]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token3) + expect(route.chainId).toEqual(1) + }) + }) + + it('can have a token as both input and output', () => { + const route = new MixedRouteSDK([pair_0_weth, pair_0_1, pair_1_weth], weth, weth) + expect(route.pools).toEqual([pair_0_weth, pair_0_1, pair_1_weth]) + expect(route.input).toEqual(weth) + expect(route.output).toEqual(weth) + }) + + describe('is backwards compatible with a 100% V3 route', () => { + it('successfully assigns the protocol', () => { + const routeOriginal = new V3RouteSDK([pool_0_1], token0, token1) + const route = new RouteV3(routeOriginal) + expect(route.protocol).toEqual(Protocol.V3) + }) + + it('inherits parameters from extended route class', () => { + const routeOriginal = new V3RouteSDK([pool_0_1], token0, token1) + const route = new RouteV3(routeOriginal) + expect(route.pools).toEqual(routeOriginal.pools) + expect(route.path).toEqual(routeOriginal.tokenPath) + expect(route.input).toEqual(routeOriginal.input) + expect(route.output).toEqual(routeOriginal.output) + expect(route.midPrice).toEqual(routeOriginal.midPrice) + expect(route.chainId).toEqual(routeOriginal.chainId) + }) + + it('can have a token as both input and output', () => { + const routeOriginal = new V3RouteSDK([pool_0_weth, pool_0_1, pool_1_weth], weth, weth) + const route = new RouteV3(routeOriginal) + expect(route.pools).toEqual([pool_0_weth, pool_0_1, pool_1_weth]) + expect(route.input).toEqual(weth) + expect(route.output).toEqual(weth) + }) + + it('supports ether input', () => { + const routeOriginal = new V3RouteSDK([pool_0_weth], ETHER, token0) + const route = new RouteV3(routeOriginal) + expect(route.pools).toEqual([pool_0_weth]) + expect(route.input).toEqual(ETHER) + expect(route.output).toEqual(token0) + }) + + it('supports ether output', () => { + const routeOriginal = new V3RouteSDK([pool_0_weth], token0, ETHER) + const route = new RouteV3(routeOriginal) + expect(route.pools).toEqual([pool_0_weth]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(ETHER) + }) + }) + + describe('#midPrice', () => { + /// @dev creating new local variables so we can easily test different pool ratios independent of other tests + const pool_0_1 = new Pool( + token0, + token1, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(1, 5), + 0, + TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(1, 5)), + [] + ) + const pool_1_2 = new Pool( + token1, + token2, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(15, 30), + 0, + TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(15, 30)), + [] + ) + const pool_0_weth = new Pool( + token0, + weth, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(3, 1), + 0, + TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(3, 1)), + [] + ) + const pool_1_weth = new Pool( + token1, + weth, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(1, 7), + 0, + TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(1, 7)), + [] + ) + + const pool_2_weth = new Pool( + token2, + weth, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(1, 8), + 0, + TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(1, 8)), + [] + ) + + const pair_0_1 = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(token1, '200')) + const pair_1_2 = new Pair(CurrencyAmount.fromRawAmount(token1, '200'), CurrencyAmount.fromRawAmount(token2, '150')) + const pair_0_2 = new Pair(CurrencyAmount.fromRawAmount(token0, '200'), CurrencyAmount.fromRawAmount(token2, '150')) + const pair_0_weth = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(weth, '100')) + const pair_1_weth = new Pair(CurrencyAmount.fromRawAmount(token1, '175'), CurrencyAmount.fromRawAmount(weth, '100')) + + describe('100% V3 pool route', () => { + it('correct for 0 -> 1', () => { + const routeV3SDK = new V3RouteSDK([pool_0_1], token0, token1) + const route = new MixedRouteSDK([pool_0_1], token0, token1) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.2000') + expect(route.midPrice.baseCurrency.equals(token0)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token1)).toEqual(true) + }) + + it('is cached', () => { + const routeOriginal = new MixedRouteSDK([pool_0_1], token0, token1) + const route = new MixedRoute(routeOriginal) + expect(route.midPrice).toStrictEqual(route.midPrice) + }) + + it('correct for 1 -> 0', () => { + const routeV3SDK = new V3RouteSDK([pool_0_1], token1, token0) + const route = new MixedRouteSDK([pool_0_1], token1, token0) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('5.0000') + expect(route.midPrice.baseCurrency.equals(token1)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token0)).toEqual(true) + }) + + it('correct for 0 -> 1 -> 2', () => { + /** + * pool_0_1 mid price = 1/5 = 0.2 + * pool_1_2 mid price = 15/30 = 0.5 + */ + const routeV3SDK = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const route = new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.1000') + expect(route.midPrice.baseCurrency.equals(token0)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token2)).toEqual(true) + }) + + it('correct for 2 -> 1 -> 0', () => { + const routeV3SDK = new V3RouteSDK([pool_1_2, pool_0_1], token2, token0) + const route = new MixedRouteSDK([pool_1_2, pool_0_1], token2, token0) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('10.0000') + expect(route.midPrice.baseCurrency.equals(token2)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token0)).toEqual(true) + }) + + it('correct for ether -> 0', () => { + const routeV3SDK = new V3RouteSDK([pool_0_weth], ETHER, token0) + const route = new MixedRouteSDK([pool_0_weth], ETHER, token0) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.3333') + expect(route.midPrice.baseCurrency.equals(ETHER)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token0)).toEqual(true) + }) + + it('correct for 1 -> weth', () => { + const routeV3SDK = new V3RouteSDK([pool_1_weth], token1, weth) + const route = new MixedRouteSDK([pool_1_weth], token1, weth) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.1429') + expect(route.midPrice.baseCurrency.equals(token1)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(weth)).toEqual(true) + }) + + it('correct for ether -> 0 -> 1 -> weth', () => { + const routeV3SDK = new V3RouteSDK([pool_0_weth, pool_0_1, pool_1_weth], ETHER, weth) + const route = new MixedRouteSDK([pool_0_weth, pool_0_1, pool_1_weth], ETHER, weth) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toSignificant(4)).toEqual('0.009524') + expect(route.midPrice.baseCurrency.equals(ETHER)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(weth)).toEqual(true) + }) + + it('correct for weth -> 0 -> 1 -> ether', () => { + const routeV3SDK = new V3RouteSDK([pool_0_weth, pool_0_1, pool_1_weth], weth, ETHER) + const route = new MixedRouteSDK([pool_0_weth, pool_0_1, pool_1_weth], weth, ETHER) + expect(route.midPrice.toFixed(4)).toEqual(routeV3SDK.midPrice.toFixed(4)) + expect(route.midPrice.toSignificant(4)).toEqual('0.009524') + expect(route.midPrice.baseCurrency.equals(weth)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(ETHER)).toEqual(true) + }) + }) + + describe('100% V2 pair route', () => { + it('correct for 0 -> 1', () => { + const routeV2SDK = new V2RouteSDK([pair_0_1], token0, token1) + const route = new MixedRouteSDK([pair_0_1], token0, token1) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('2.0000') + }) + + it('is cached', () => { + const route = new MixedRouteSDK([pair_0_1], token0, token1) + expect(route.midPrice).toStrictEqual(route.midPrice) + }) + + it('correct for 1 -> 0', () => { + const routeV2SDK = new V2RouteSDK([pair_0_1], token1, token0) + const route = new MixedRouteSDK([pair_0_1], token1, token0) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.5000') + expect(route.midPrice.baseCurrency.equals(token1)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token0)).toEqual(true) + }) + + it('correct for 0 -> 1 -> 2', () => { + /** + * pair_0_1 mid price = 200 / 100 = 2 + * pair_1_2 mid price = 150 / 200 = 0.75 + * + * 2 * 0.75 = 1.5 + */ + const routeV2SDK = new V2RouteSDK([pair_0_1, pair_1_2], token0, token2) + const route = new MixedRouteSDK([pair_0_1, pair_1_2], token0, token2) + expect(routeV2SDK.midPrice).toEqual(route.midPrice) + expect(route.midPrice.toFixed(4)).toEqual('1.5000') + expect(route.midPrice.baseCurrency.equals(token0)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token2)).toEqual(true) + }) + + it('correct for 2 -> 1 -> 0', () => { + const routeV2SDK = new V2RouteSDK([pair_1_2, pair_0_1], token2, token0) + const route = new MixedRouteSDK([pair_1_2, pair_0_1], token2, token0) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.6667') + expect(route.midPrice.baseCurrency.equals(token2)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token0)).toEqual(true) + }) + + it('correct for ether -> 0', () => { + const routeV2SDK = new V2RouteSDK([pair_0_weth], ETHER, token0) + const route = new MixedRouteSDK([pair_0_weth], ETHER, token0) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('1.0000') + expect(route.midPrice.baseCurrency.equals(ETHER)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(token0)).toEqual(true) + }) + + it('correct for 1 -> weth', () => { + const routeV2SDK = new V2RouteSDK([pair_1_weth], token1, weth) + const route = new MixedRouteSDK([pair_1_weth], token1, weth) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toFixed(4)).toEqual('0.5714') + expect(route.midPrice.baseCurrency.equals(token1)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(weth)).toEqual(true) + }) + + it('correct for ether -> 0 -> 1 -> weth', () => { + const routeV2SDK = new V2RouteSDK([pair_0_weth, pair_0_1, pair_1_weth], ETHER, weth) + const route = new MixedRouteSDK([pair_0_weth, pair_0_1, pair_1_weth], ETHER, weth) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toSignificant(4)).toEqual('1.143') + expect(route.midPrice.baseCurrency.equals(ETHER)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(weth)).toEqual(true) + }) + + it('correct for weth -> 0 -> 1 -> ether', () => { + const routeV2SDK = new V2RouteSDK([pair_0_weth, pair_0_1, pair_1_weth], weth, ETHER) + const route = new MixedRouteSDK([pair_0_weth, pair_0_1, pair_1_weth], weth, ETHER) + expect(routeV2SDK.midPrice.toFixed(4)).toEqual(route.midPrice.toFixed(4)) + expect(route.midPrice.toSignificant(4)).toEqual('1.143') + expect(route.midPrice.baseCurrency.equals(weth)).toEqual(true) + expect(route.midPrice.quoteCurrency.equals(ETHER)).toEqual(true) + }) + }) + + describe('mixed route', () => { + it('correct for 0 -[V3]-> 1 -[V2]-> 2', () => { + // pool_0_1 midPrice = 0.2 + // pair_1_2 1 < 2, so token0 = t1, token1 = 2, so 150/200 = 0.75 + // so midPoint = 0.2 * 0.75 + + const route = new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2) + expect(route.midPrice.toFixed(4)).toEqual('0.1500') + }) + + it('correct for 0 -[V3]-> 1 -[V2]-> 2, 1 for 2', () => { + // nextInput != token0, so 0/1 pool so 5 + // nextInput == token0, pair so 1/0 so 150/200 + // = 5 * 0.75 = 3.75 + + const route = new MixedRouteSDK([pool_0_1, pair_0_2], token1, token2) + expect(route.midPrice.toFixed(4)).toEqual('3.7500') + }) + + it('correct for 0 -[V2]-> 1 -[V2]-> weth -[V3]-> 2', () => { + const route = new MixedRouteSDK([pair_0_1, pair_1_weth, pool_2_weth], token0, token2) + /** + * pair_0_1 midPrice = 200 / 100 = 2 + * nextInput = 1 -> pair_1_weth midPrice = 100 / 175 = 0.5714 + * nextInput = weth -> pool_2_weth midPrice = is 1 -> 0 so 8/1 = 8 + * so midPoint = 2 * 0.5714 * 8 = 9.1429 + */ + + expect(route.midPrice.toFixed(4)).toEqual('9.1429') + }) + }) + }) + + describe('partitionMixedRouteByProtocol', () => { + it('returns correct for single pool', () => { + const route = new MixedRouteSDK([pool_0_1], token0, token1) + expect(partitionMixedRouteByProtocol(route)).toStrictEqual([[pool_0_1]]) + }) + it('returns correct for single pool', () => { + const route = new MixedRouteSDK([pair_0_1], token0, token1) + expect(partitionMixedRouteByProtocol(route)).toStrictEqual([[pair_0_1]]) + }) + + it('returns correct for route of all the v3 pools', () => { + const route = new MixedRouteSDK([pool_0_1, pool_1_weth, pool_2_weth], token0, token2) + const result = partitionMixedRouteByProtocol(route) + expect(result.length).toEqual(1) + expect(result[0].length).toEqual(3) + expect(result).toStrictEqual([[pool_0_1, pool_1_weth, pool_2_weth]]) + }) + + it('consecutive pair in middle of two pools', () => { + const route: MixedRouteSDK = new MixedRouteSDK( + [pool_0_1, pair_1_weth, pair_weth_2, pool_2_3], + token0, + token3 + ) + const result = partitionMixedRouteByProtocol(route) + expect(result.length).toEqual(3) + expect(result[0][0]).toStrictEqual(pool_0_1) + expect(result[1].length).toEqual(2) + const referenceSecondPart = [pair_1_weth, pair_weth_2] + result[1].forEach((pair, index) => { + expect(pair).toStrictEqual(referenceSecondPart[index]) + }) + expect(result[2][0]).toStrictEqual(pool_2_3) + }) + it('consecutive pair at the end', () => { + const route: MixedRouteSDK = new MixedRouteSDK( + [pool_0_1, pair_1_weth, pair_weth_2, pair_2_3], + token0, + token3 + ) + const result = partitionMixedRouteByProtocol(route) + expect(result.length).toEqual(2) + expect(result[0][0]).toStrictEqual(pool_0_1) + const referenceSecondPart = [pair_1_weth, pair_weth_2, pair_2_3] + result[1].forEach((pair, i) => { + expect(pair).toStrictEqual(referenceSecondPart[i]) + }) + }) + it('consecutive pair at the beginning', () => { + const route: MixedRouteSDK = new MixedRouteSDK( + [pair_0_1, pair_1_weth, pair_weth_2, pool_2_3], + token0, + token3 + ) + const result = partitionMixedRouteByProtocol(route) + expect(result.length).toEqual(2) + const referenceFirstPart = [pair_0_1, pair_1_weth, pair_weth_2] + result[0].forEach((pair, i) => { + expect(pair).toStrictEqual(referenceFirstPart[i]) + }) + expect(result[1][0]).toStrictEqual(pool_2_3) + }) + }) +}) diff --git a/src/entities/mixedRoute/route.ts b/src/entities/mixedRoute/route.ts new file mode 100644 index 0000000..aa213a0 --- /dev/null +++ b/src/entities/mixedRoute/route.ts @@ -0,0 +1,92 @@ +import invariant from 'tiny-invariant' + +import { Currency, Price, Token } from '@uniswap/sdk-core' +import { Pool } from '@uniswap/v3-sdk' +import { Pair } from '@uniswap/v2-sdk' + +type TPool = Pair | Pool + +/** + * Represents a list of pools or pairs through which a swap can occur + * @template TInput The input token + * @template TOutput The output token + */ +export class MixedRouteSDK { + public readonly pools: TPool[] + public readonly path: Token[] + public readonly input: TInput + public readonly output: TOutput + + private _midPrice: Price | null = null + + /** + * Creates an instance of route. + * @param pools An array of `TPool` objects (pools or pairs), ordered by the route the swap will take + * @param input The input token + * @param output The output token + */ + public constructor(pools: TPool[], input: TInput, output: TOutput) { + invariant(pools.length > 0, 'POOLS') + + const chainId = pools[0].chainId + const allOnSameChain = pools.every((pool) => pool.chainId === chainId) + invariant(allOnSameChain, 'CHAIN_IDS') + + const wrappedInput = input.wrapped + invariant(pools[0].involvesToken(wrappedInput), 'INPUT') + + invariant(pools[pools.length - 1].involvesToken(output.wrapped), 'OUTPUT') + + /** + * Normalizes token0-token1 order and selects the next token/fee step to add to the path + * */ + const tokenPath: Token[] = [wrappedInput] + for (const [i, pool] of pools.entries()) { + const currentInputToken = tokenPath[i] + invariant(currentInputToken.equals(pool.token0) || currentInputToken.equals(pool.token1), 'PATH') + const nextToken = currentInputToken.equals(pool.token0) ? pool.token1 : pool.token0 + tokenPath.push(nextToken) + } + + this.pools = pools + this.path = tokenPath + this.input = input + this.output = output ?? tokenPath[tokenPath.length - 1] + } + + public get chainId(): number { + return this.pools[0].chainId + } + + /** + * Returns the mid price of the route + */ + public get midPrice(): Price { + if (this._midPrice !== null) return this._midPrice + + const price = this.pools.slice(1).reduce( + ({ nextInput, price }, pool) => { + return nextInput.equals(pool.token0) + ? { + nextInput: pool.token1, + price: price.multiply(pool.token0Price), + } + : { + nextInput: pool.token0, + price: price.multiply(pool.token1Price), + } + }, + this.pools[0].token0.equals(this.input.wrapped) + ? { + nextInput: this.pools[0].token1, + price: this.pools[0].token0Price, + } + : { + nextInput: this.pools[0].token0, + price: this.pools[0].token1Price, + } + ).price + + return (this._midPrice = new Price(this.input, this.output, price.denominator, price.numerator)) + } +} diff --git a/src/entities/mixedRoute/trade.test.ts b/src/entities/mixedRoute/trade.test.ts new file mode 100644 index 0000000..8115ca8 --- /dev/null +++ b/src/entities/mixedRoute/trade.test.ts @@ -0,0 +1,1385 @@ +import { Percent, Price, sqrt, Token, CurrencyAmount, TradeType, WETH9, Ether, Currency } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' +import { encodeSqrtRatioX96, FeeAmount, nearestUsableTick, Pool, TickMath, TICK_SPACINGS } from '@uniswap/v3-sdk' +import JSBI from 'jsbi' +import { MixedRouteSDK } from './route' +import { MixedRouteTrade } from './trade' + +describe('MixedRouteTrade', () => { + const ETHER = Ether.onChain(1) + const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'token0') + const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1', 'token1') + const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2', 'token2') + const token3 = new Token(1, '0x0000000000000000000000000000000000000004', 18, 't3', 'token3') + + function v2StylePool( + reserve0: CurrencyAmount, + reserve1: CurrencyAmount, + feeAmount: FeeAmount = FeeAmount.MEDIUM + ) { + const sqrtRatioX96 = encodeSqrtRatioX96(reserve1.quotient, reserve0.quotient) + const liquidity = sqrt(JSBI.multiply(reserve0.quotient, reserve1.quotient)) + return new Pool( + reserve0.currency, + reserve1.currency, + feeAmount, + sqrtRatioX96, + liquidity, + TickMath.getTickAtSqrtRatio(sqrtRatioX96), + [ + { + index: nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount]), + liquidityNet: liquidity, + liquidityGross: liquidity, + }, + { + index: nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount]), + liquidityNet: JSBI.multiply(liquidity, JSBI.BigInt(-1)), + liquidityGross: liquidity, + }, + ] + ) + } + + const pool_0_1 = v2StylePool( + CurrencyAmount.fromRawAmount(token0, 100000), + CurrencyAmount.fromRawAmount(token1, 100000) + ) + const pool_0_2 = v2StylePool( + CurrencyAmount.fromRawAmount(token0, 100000), + CurrencyAmount.fromRawAmount(token2, 110000) + ) + const pool_0_3 = v2StylePool( + CurrencyAmount.fromRawAmount(token0, 100000), + CurrencyAmount.fromRawAmount(token3, 90000) + ) + const pool_1_2 = v2StylePool( + CurrencyAmount.fromRawAmount(token1, 120000), + CurrencyAmount.fromRawAmount(token2, 100000) + ) + const pool_1_3 = v2StylePool( + CurrencyAmount.fromRawAmount(token1, 120000), + CurrencyAmount.fromRawAmount(token3, 130000) + ) + + const pool_weth_0 = v2StylePool( + CurrencyAmount.fromRawAmount(WETH9[1], JSBI.BigInt(100000)), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100000)) + ) + + const pool_weth_1 = v2StylePool( + CurrencyAmount.fromRawAmount(WETH9[1], JSBI.BigInt(100000)), + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100000)) + ) + + const pool_weth_2 = v2StylePool( + CurrencyAmount.fromRawAmount(WETH9[1], JSBI.BigInt(100000)), + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(100000)) + ) + + const pair_0_1 = new Pair( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)), + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)) + ) + const pair_0_2 = new Pair( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)), + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(1100)) + ) + const pair_0_3 = new Pair( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)), + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(900)) + ) + const pair_1_2 = new Pair( + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1200)), + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(1000)) + ) + const pair_1_3 = new Pair( + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1200)), + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(1300)) + ) + + const pair_weth_0 = new Pair( + CurrencyAmount.fromRawAmount(WETH9[1], JSBI.BigInt(1000)), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + ) + + const empty_pair_0_1 = new Pair( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(0)), + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(0)) + ) + + /// @dev copied over from v3-sdk trade.test.ts + describe('is backwards compatible with pure v3 routes', () => { + describe('#fromRoute', () => { + it('can be constructed with ETHER as input', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_weth_0], ETHER, token0), + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token0) + }) + it('can be constructed with ETHER as output for exact input', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_weth_0], token0, ETHER), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(ETHER) + }) + + it('throws regardless for exact output', async () => { + await expect( + MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_weth_0], ETHER, token0), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + TradeType.EXACT_OUTPUT + ) + ).rejects.toThrow('TRADE_TYPE') + }) + }) + + describe('#fromRoutes', () => { + it('can be constructed with ETHER as input with multiple routes', async () => { + const trade = await MixedRouteTrade.fromRoutes( + [ + { + amount: CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(10000)), + route: new MixedRouteSDK([pool_weth_0], ETHER, token0), + }, + ], + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token0) + }) + + it('can be constructed with ETHER as output for exact input with multiple routes', async () => { + const trade = await MixedRouteTrade.fromRoutes( + [ + { + amount: CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(3000)), + route: new MixedRouteSDK([pool_weth_0], token0, ETHER), + }, + { + amount: CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(7000)), + route: new MixedRouteSDK([pool_0_1, pool_weth_1], token0, ETHER), + }, + ], + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(ETHER) + }) + + it('throws if pools are re-used between routes', async () => { + await expect( + MixedRouteTrade.fromRoutes( + [ + { + amount: CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(4500)), + route: new MixedRouteSDK([pool_0_1, pool_weth_1], token0, ETHER), + }, + { + amount: CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(5500)), + route: new MixedRouteSDK([pool_0_1, pool_1_2, pool_weth_2], token0, ETHER), + }, + ], + TradeType.EXACT_INPUT + ) + ).rejects.toThrow('POOLS_DUPLICATED') + }) + + it('throws if created with exact output', async () => { + await expect( + MixedRouteTrade.fromRoutes( + [ + { + amount: CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(10000)), + route: new MixedRouteSDK([pool_weth_0], ETHER, token0), + }, + ], + TradeType.EXACT_OUTPUT + ) + ).rejects.toThrow('TRADE_TYPE') + }) + }) + + describe('#createUncheckedTrade', () => { + it('throws if input currency does not match route', () => { + expect(() => + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 10000), + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('INPUT_CURRENCY_MATCH') + }) + it('throws if output currency does not match route', () => { + expect(() => + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('OUTPUT_CURRENCY_MATCH') + }) + it('throws if tradeType is exactOutput', () => { + try { + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1], token1, token0), + inputAmount: CurrencyAmount.fromRawAmount(token1, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token0, 100000), + tradeType: TradeType.EXACT_OUTPUT, + }) + } catch (err) { + // @ts-ignore + expect(err.message).toEqual('Invariant failed: TRADE_TYPE') + } + }) + it('can create an exact input trade without simulating', () => { + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 100000), + tradeType: TradeType.EXACT_INPUT, + }) + }) + }) + describe('#createUncheckedTradeWithMultipleRoutes', () => { + it('throws if input currency does not match route with multiple routes', () => { + expect(() => + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_1_2], token2, token1), + inputAmount: CurrencyAmount.fromRawAmount(token2, 2000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 2000), + }, + { + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token2, 8000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 8000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('INPUT_CURRENCY_MATCH') + }) + it('throws if output currency does not match route with multiple routes', () => { + expect(() => + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + }, + { + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('OUTPUT_CURRENCY_MATCH') + }) + + it('throws if tradeType is exact output', () => { + try { + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 5000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 50000), + }, + { + route: new MixedRouteSDK([pool_0_2, pool_1_2], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 5000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 50000), + }, + ], + tradeType: TradeType.EXACT_OUTPUT, + }) + } catch (err) { + // @ts-ignore + expect(err.message).toEqual('Invariant failed: TRADE_TYPE') + } + }) + + it('can create an exact input trade without simulating with multiple routes', () => { + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 5000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 50000), + }, + { + route: new MixedRouteSDK([pool_0_2, pool_1_2], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 5000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 50000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + }) + }) + + describe('#route and #swaps', () => { + const singleRoute = MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + tradeType: TradeType.EXACT_INPUT, + }) + const multiRoute = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 35), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 34), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('can access route for single route trade if less than 0', () => { + expect(singleRoute.swaps).toBeDefined() + }) + it('can access routes for both single and multi route trades', () => { + expect(singleRoute.swaps).toBeDefined() + expect(singleRoute.swaps).toHaveLength(1) + expect(multiRoute.swaps).toBeDefined() + expect(multiRoute.swaps).toHaveLength(2) + }) + it('throws if access route on multi route trade', () => { + expect(() => multiRoute.route).toThrow('MULTIPLE_ROUTES') + }) + }) + + describe('#worstExecutionPrice', () => { + describe('tradeType = EXACT_INPUT', () => { + const exactIn = MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + tradeType: TradeType.EXACT_INPUT, + }) + const exactInMultiRoute = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 35), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 34), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('throws if less than 0', () => { + expect(() => exactIn.minimumAmountOut(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') + }) + it('returns exact if 0', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(exactIn.executionPrice) + }) + it('returns exact if nonzero', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) + expect(exactIn.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) + expect(exactIn.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 100, 23)) + }) + it('returns exact if nonzero with multiple routes', () => { + expect(exactInMultiRoute.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) + expect(exactInMultiRoute.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) + expect(exactInMultiRoute.worstExecutionPrice(new Percent(200, 100))).toEqual( + new Price(token0, token2, 100, 23) + ) + }) + }) + }) + + describe('#priceImpact', () => { + describe('100% v3 route', () => { + describe('tradeType = EXACT_INPUT', () => { + const exactIn = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + const exactInMultipleRoutes = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 90), + outputAmount: CurrencyAmount.fromRawAmount(token2, 62), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10), + outputAmount: CurrencyAmount.fromRawAmount(token2, 7), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('is cached', () => { + expect(exactIn.priceImpact === exactIn.priceImpact).toStrictEqual(true) + }) + it('is correct', () => { + expect(exactIn.priceImpact.toSignificant(3)).toEqual('17.2') + }) + + it('is cached with multiple routes', () => { + expect(exactInMultipleRoutes.priceImpact === exactInMultipleRoutes.priceImpact).toStrictEqual(true) + }) + it('is correct with multiple routes', async () => { + expect(exactInMultipleRoutes.priceImpact.toSignificant(3)).toEqual('19.8') + }) + }) + }) + + describe('mixed route', () => { + describe('tradeType = EXACT_INPUT', () => { + const exactIn = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + const exactInMultipleRoutes = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 90), + outputAmount: CurrencyAmount.fromRawAmount(token2, 62), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10), + outputAmount: CurrencyAmount.fromRawAmount(token2, 7), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('is cached', () => { + expect(exactIn.priceImpact === exactIn.priceImpact).toStrictEqual(true) + }) + it('is correct', () => { + expect(exactIn.priceImpact.toSignificant(3)).toEqual('17.2') + }) + + it('is cached with multiple routes', () => { + expect(exactInMultipleRoutes.priceImpact === exactInMultipleRoutes.priceImpact).toStrictEqual(true) + }) + it('is correct with multiple routes', async () => { + expect(exactInMultipleRoutes.priceImpact.toSignificant(3)).toEqual('19.8') + }) + }) + }) + }) + + describe('#bestTradeExactIn', () => { + it('throws with empty pools', async () => { + await expect( + MixedRouteTrade.bestTradeExactIn([], CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), token2) + ).rejects.toThrow('POOLS') + }) + it('throws with max hops of 0', async () => { + await expect( + MixedRouteTrade.bestTradeExactIn( + [pool_0_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + token2, + { + maxHops: 0, + } + ) + ).rejects.toThrow('MAX_HOPS') + }) + + it('provides best route', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pool_0_2, pool_1_2], + CurrencyAmount.fromRawAmount(token0, 10000), + token2 + ) + expect(result).toHaveLength(2) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + expect(result[0].inputAmount.equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)))).toBeTruthy() + expect(result[0].outputAmount.equalTo(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(9971)))).toBeTruthy() + expect(result[1].swaps[0].route.pools).toHaveLength(2) // 0 -> 1 -> 2 at 12:12:10 + expect(result[1].swaps[0].route.path).toEqual([token0, token1, token2]) + expect(result[1].inputAmount.equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)))).toBeTruthy() + expect(result[1].outputAmount.equalTo(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(7004)))).toBeTruthy() + }) + + it('respects maxHops', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pool_0_2, pool_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2, + { maxHops: 1 } + ) + expect(result).toHaveLength(1) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + }) + + it('insufficient input for one pool', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pool_0_2, pool_1_2], + CurrencyAmount.fromRawAmount(token0, 1), + token2 + ) + expect(result).toHaveLength(2) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + expect(result[0].outputAmount).toEqual(CurrencyAmount.fromRawAmount(token2, 0)) + }) + + it('respects n', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pool_0_2, pool_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2, + { maxNumResults: 1 } + ) + + expect(result).toHaveLength(1) + }) + + it('no path', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pool_0_3, pool_1_3], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2 + ) + expect(result).toHaveLength(0) + }) + + it('works for ETHER currency input', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_weth_0, pool_0_1, pool_0_3, pool_1_3], + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(100)), + token3 + ) + expect(result).toHaveLength(2) + expect(result[0].inputAmount.currency).toEqual(ETHER) + expect(result[0].swaps[0].route.path).toEqual([WETH9[1], token0, token1, token3]) + expect(result[0].outputAmount.currency).toEqual(token3) + expect(result[1].inputAmount.currency).toEqual(ETHER) + expect(result[1].swaps[0].route.path).toEqual([WETH9[1], token0, token3]) + expect(result[1].outputAmount.currency).toEqual(token3) + }) + + it('works for ETHER currency output', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_weth_0, pool_0_1, pool_0_3, pool_1_3], + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(100)), + ETHER + ) + expect(result).toHaveLength(2) + expect(result[0].inputAmount.currency).toEqual(token3) + expect(result[0].swaps[0].route.path).toEqual([token3, token0, WETH9[1]]) + expect(result[0].outputAmount.currency).toEqual(ETHER) + expect(result[1].inputAmount.currency).toEqual(token3) + expect(result[1].swaps[0].route.path).toEqual([token3, token1, token0, WETH9[1]]) + expect(result[1].outputAmount.currency).toEqual(ETHER) + }) + }) + + describe('#maximumAmountIn', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + beforeEach(async () => { + exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + }) + it('throws if less than 0', () => { + expect(() => exactIn.maximumAmountIn(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( + 'SLIPPAGE_TOLERANCE' + ) + }) + it('returns exact if 0', () => { + expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactIn.inputAmount) + }) + it('returns exact if nonzero', () => { + expect( + exactIn + .maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100))) + .equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + ).toBeTruthy() + expect( + exactIn + .maximumAmountIn(new Percent(JSBI.BigInt(5), JSBI.BigInt(100))) + .equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + ).toBeTruthy() + expect( + exactIn + .maximumAmountIn(new Percent(JSBI.BigInt(200), JSBI.BigInt(100))) + .equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + ).toBeTruthy() + }) + }) + }) + + describe('#minimumAmountOut', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + beforeEach( + async () => + (exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, 10000), + TradeType.EXACT_INPUT + )) + ) + + it('throws if less than 0', () => { + expect(() => exactIn.minimumAmountOut(new Percent(JSBI.BigInt(-1), 100))).toThrow('SLIPPAGE_TOLERANCE') + }) + + it('returns exact if 0', () => { + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), 10000))).toEqual(exactIn.outputAmount) + }) + + it('returns exact if nonzero', () => { + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), 100))).toEqual( + CurrencyAmount.fromRawAmount(token2, 7004) + ) + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(5), 100))).toEqual( + CurrencyAmount.fromRawAmount(token2, 6670) + ) + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(200), 100))).toEqual( + CurrencyAmount.fromRawAmount(token2, 2334) + ) + }) + }) + }) + }) + + /// @dev copied over from v2-sdk trade.test.ts + describe('is backwards compatible with pure v2 routes', () => { + it('can be constructed with ETHER as input', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_weth_0], ETHER, token0), + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token0) + }) + + it('can be constructed with ETHER as output for exact input', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_weth_0], token0, ETHER), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(ETHER) + }) + + describe('#bestTradeExactIn', () => { + it('throws with empty pairs', async () => { + await expect( + MixedRouteTrade.bestTradeExactIn([], CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), token2) + ).rejects.toThrow('POOLS') + }) + it('throws with max hops of 0', async () => { + await expect( + MixedRouteTrade.bestTradeExactIn([pair_0_2], CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), token2, { + maxHops: 0, + }) + ).rejects.toThrow('MAX_HOPS') + }) + + it('provides best route', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_0_1, pair_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + token2 + ) + expect(result).toHaveLength(2) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + expect(result[0].inputAmount).toEqual(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + expect(result[0].outputAmount).toEqual(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(99))) + expect(result[1].swaps[0].route.pools).toHaveLength(2) // 0 -> 1 -> 2 at 12:12:10 + expect(result[1].swaps[0].route.path).toEqual([token0, token1, token2]) + expect(result[1].inputAmount).toEqual(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + expect(result[1].outputAmount).toEqual(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(69))) + }) + + it('doesnt throw for zero liquidity pairs', async () => { + expect( + await MixedRouteTrade.bestTradeExactIn( + [empty_pair_0_1], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + token1 + ) + ).toHaveLength(0) + }) + + it('respects maxHops', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_0_1, pair_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2, + { maxHops: 1 } + ) + expect(result).toHaveLength(1) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + }) + + it('insufficient input for one pair', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_0_1, pair_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1)), + token2 + ) + expect(result).toHaveLength(1) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + expect(result[0].outputAmount).toEqual(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(1))) + }) + + it('respects n', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_0_1, pair_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2, + { maxNumResults: 1 } + ) + + expect(result).toHaveLength(1) + }) + + it('no path', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_0_1, pair_0_3, pair_1_3], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2 + ) + expect(result).toHaveLength(0) + }) + + it('works for ETHER currency input', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_weth_0, pair_0_1, pair_0_3, pair_1_3], + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(100)), + token3 + ) + expect(result).toHaveLength(2) + expect(result[0].inputAmount.currency).toEqual(ETHER) + expect(result[0].swaps[0].route.path).toEqual([WETH9[1], token0, token1, token3]) + expect(result[0].outputAmount.currency).toEqual(token3) + expect(result[1].inputAmount.currency).toEqual(ETHER) + expect(result[1].swaps[0].route.path).toEqual([WETH9[1], token0, token3]) + expect(result[1].outputAmount.currency).toEqual(token3) + }) + it('works for ETHER currency output', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_weth_0, pair_0_1, pair_0_3, pair_1_3], + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(100)), + ETHER + ) + expect(result).toHaveLength(2) + expect(result[0].inputAmount.currency).toEqual(token3) + expect(result[0].swaps[0].route.path).toEqual([token3, token0, WETH9[1]]) + expect(result[0].outputAmount.currency).toEqual(ETHER) + expect(result[1].inputAmount.currency).toEqual(token3) + expect(result[1].swaps[0].route.path).toEqual([token3, token1, token0, WETH9[1]]) + expect(result[1].outputAmount.currency).toEqual(ETHER) + }) + }) + + describe('#maximumAmountIn', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + beforeAll(async () => { + exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + }) + + it('throws if less than 0', () => { + expect(() => exactIn.maximumAmountIn(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( + 'SLIPPAGE_TOLERANCE' + ) + }) + it('returns exact if 0', () => { + expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactIn.inputAmount) + }) + it('returns exact if nonzero', () => { + expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + ) + expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(5), JSBI.BigInt(100)))).toEqual( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + ) + expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(200), JSBI.BigInt(100)))).toEqual( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + ) + }) + }) + }) + + describe('#minimumAmountOut', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + beforeAll(async () => { + exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + }) + it('throws if less than 0', () => { + expect(() => exactIn.minimumAmountOut(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( + 'SLIPPAGE_TOLERANCE' + ) + }) + it('returns exact if 0', () => { + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactIn.outputAmount) + }) + it('returns exact if nonzero', () => { + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual( + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(69)) + ) + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(5), JSBI.BigInt(100)))).toEqual( + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(65)) + ) + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(200), JSBI.BigInt(100)))).toEqual( + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(23)) + ) + }) + }) + }) + + describe('#worstExecutionPrice', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + beforeAll(async () => { + exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, 100), + TradeType.EXACT_INPUT + ) + }) + it('throws if less than 0', () => { + expect(() => exactIn.minimumAmountOut(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') + }) + it('returns exact if 0', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(exactIn.executionPrice) + }) + it('returns exact if nonzero', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) + expect(exactIn.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) + expect(exactIn.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 100, 23)) + }) + }) + }) + }) + + describe('multihop v2 + v3', () => { + describe('#fromRoute', () => { + it('can be constructed with ETHER as input', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_weth_0, pair_0_1], ETHER, token1), + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token1) + }) + it('can be constructed with ETHER as output for exact input', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pool_weth_0], token1, ETHER), + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(10000)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token1) + expect(trade.outputAmount.currency).toEqual(ETHER) + }) + it('allows using input tokens as intermediary', async () => { + const trade = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pool_0_1, pair_0_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(token2) + }) + }) + + describe('#fromRoutes', () => { + it('can be constructed with ETHER as input with multiple routes', async () => { + const trade = await MixedRouteTrade.fromRoutes( + [ + { + amount: CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(10000)), + route: new MixedRouteSDK([pool_weth_0, pair_0_1], ETHER, token1), + }, + ], + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token1) + }) + + it('can be constructed with ETHER as output for exact input with multiple routes', async () => { + const trade = await MixedRouteTrade.fromRoutes( + [ + { + amount: CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(3000)), + route: new MixedRouteSDK([pair_0_1, pool_weth_0], token1, ETHER), + }, + { + amount: CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(7000)), + route: new MixedRouteSDK([pair_1_2, pool_weth_2], token1, ETHER), + }, + ], + TradeType.EXACT_INPUT + ) + expect(trade.inputAmount.currency).toEqual(token1) + expect(trade.outputAmount.currency).toEqual(ETHER) + }) + + /// no test for pool duplication because both v3 and v2 tests above cover it + }) + + describe('#createUncheckedTrade', () => { + it('throws if input currency does not match route', () => { + expect(() => + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 10000), + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('INPUT_CURRENCY_MATCH') + }) + it('throws if output currency does not match route', () => { + expect(() => + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token3, 10000), + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('OUTPUT_CURRENCY_MATCH') + }) + it('can create an exact input trade without simulating', () => { + MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token2, 100000), + tradeType: TradeType.EXACT_INPUT, + }) + }) + }) + + describe('#createUncheckedTradeWithMultipleRoutes', () => { + it('throws if input currency does not match route with multiple routes', () => { + expect(() => + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_1_2], token2, token1), + inputAmount: CurrencyAmount.fromRawAmount(token2, 2000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 2000), + }, + { + route: new MixedRouteSDK([pair_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token2, 8000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 8000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('INPUT_CURRENCY_MATCH') + }) + it('throws if output currency does not match route with multiple routes', () => { + expect(() => + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + }, + { + route: new MixedRouteSDK([pair_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10000), + outputAmount: CurrencyAmount.fromRawAmount(token2, 10000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + ).toThrow('OUTPUT_CURRENCY_MATCH') + }) + + it('can create an exact input trade without simulating with multiple routes', () => { + MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 5000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 50000), + }, + { + route: new MixedRouteSDK([pool_0_2, pair_1_2], token0, token1), + inputAmount: CurrencyAmount.fromRawAmount(token0, 5000), + outputAmount: CurrencyAmount.fromRawAmount(token1, 50000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + }) + }) + + describe('#route and #swaps', () => { + const singleRoute = MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + tradeType: TradeType.EXACT_INPUT, + }) + const multiRoute = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 35), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 34), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('can access route for single route trade if less than 0', () => { + expect(singleRoute.route).toBeDefined() + }) + it('can access routes for both single and multi route trades', () => { + expect(singleRoute.swaps).toBeDefined() + expect(singleRoute.swaps).toHaveLength(1) + expect(multiRoute.swaps).toBeDefined() + expect(multiRoute.swaps).toHaveLength(2) + }) + it('throws if access route on multi route trade', () => { + expect(() => multiRoute.route).toThrow('MULTIPLE_ROUTES') + }) + }) + + describe('#worstExecutionPrice', () => { + describe('tradeType = EXACT_INPUT', () => { + const exactIn = MixedRouteTrade.createUncheckedTrade({ + route: new MixedRouteSDK([pair_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + tradeType: TradeType.EXACT_INPUT, + }) + const exactInMultiRoute = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pair_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 35), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 34), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('throws if less than 0', () => { + expect(() => exactIn.minimumAmountOut(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') + }) + it('returns exact if 0', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(exactIn.executionPrice) + }) + it('returns exact if nonzero', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) + expect(exactIn.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) + expect(exactIn.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 100, 23)) + }) + it('returns exact if nonzero with multiple routes', () => { + expect(exactInMultiRoute.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) + expect(exactInMultiRoute.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) + expect(exactInMultiRoute.worstExecutionPrice(new Percent(200, 100))).toEqual( + new Price(token0, token2, 100, 23) + ) + }) + }) + }) + + describe('#priceImpact', () => { + describe('tradeType = EXACT_INPUT', () => { + const exactIn = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pair_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + const exactInMultipleRoutes = MixedRouteTrade.createUncheckedTradeWithMultipleRoutes({ + routes: [ + { + route: new MixedRouteSDK([pair_0_1, pool_1_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 90), + outputAmount: CurrencyAmount.fromRawAmount(token2, 62), + }, + { + route: new MixedRouteSDK([pool_0_2], token0, token2), + inputAmount: CurrencyAmount.fromRawAmount(token0, 10), + outputAmount: CurrencyAmount.fromRawAmount(token2, 7), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + it('is cached', () => { + expect(exactIn.priceImpact === exactIn.priceImpact).toStrictEqual(true) + }) + it('is correct', () => { + expect(exactIn.priceImpact.toSignificant(3)).toEqual('17.2') + }) + + it('is cached with multiple routes', () => { + expect(exactInMultipleRoutes.priceImpact === exactInMultipleRoutes.priceImpact).toStrictEqual(true) + }) + it('is correct with multiple routes', async () => { + expect(exactInMultipleRoutes.priceImpact.toSignificant(3)).toEqual('19.8') + }) + }) + }) + + describe('#bestTradeExactIn', () => { + /// no empty check because covered by v3 backward compatibility test + + it('throws with max hops of 0', async () => { + await expect( + MixedRouteTrade.bestTradeExactIn( + [pool_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + token1, + { + maxHops: 0, + } + ) + ).rejects.toThrow('MAX_HOPS') + }) + + it('provides best route', async () => { + const large_pair_0_1 = new Pair( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100000)), + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100000)) + ) + const result = await MixedRouteTrade.bestTradeExactIn( + [large_pair_0_1, pool_0_2, pool_1_2], + CurrencyAmount.fromRawAmount(token0, 10000), + token2 + ) + + expect(result).toHaveLength(2) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + expect(result[0].inputAmount.equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)))).toBeTruthy() + expect(result[0].outputAmount.equalTo(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(9971)))).toBeTruthy() + expect(result[1].swaps[0].route.pools).toHaveLength(2) // 0 -> 1 -> 2 at 12:12:10 + expect(result[1].swaps[0].route.path).toEqual([token0, token1, token2]) + expect(result[1].inputAmount.equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)))).toBeTruthy() + expect(result[1].outputAmount.equalTo(CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(7004)))).toBeTruthy() + }) + + it('respects maxHops', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pool_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2, + { maxHops: 1 } + ) + expect(result).toHaveLength(1) + expect(result[0].swaps[0].route.pools).toHaveLength(1) // 0 -> 2 at 10:11 + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + }) + + it('insufficient input for one pool', async () => { + /// pairs are just skipped + const result = await MixedRouteTrade.bestTradeExactIn( + [pair_0_1, pool_0_2, pair_1_2], + CurrencyAmount.fromRawAmount(token0, 1), + token2 + ) + + expect(result).toHaveLength(1) + expect(result[0].swaps[0].route.pools).toHaveLength(1) + expect(result[0].swaps[0].route.path).toEqual([token0, token2]) + expect(result[0].outputAmount).toEqual(CurrencyAmount.fromRawAmount(token2, 0)) + }) + + it('respects n', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pair_0_2, pool_1_2], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2, + { maxNumResults: 1 } + ) + + expect(result).toHaveLength(1) + }) + + it('no path between v2 and v3', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_0_1, pair_0_3, pool_1_3], + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)), + token2 + ) + expect(result).toHaveLength(0) + }) + + it('works for ETHER currency input', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_weth_0, pair_0_1, pool_0_3, pair_1_3], + CurrencyAmount.fromRawAmount(Ether.onChain(1), JSBI.BigInt(100)), + token3 + ) + expect(result).toHaveLength(2) + expect(result[0].inputAmount.currency).toEqual(ETHER) + expect(result[0].swaps[0].route.path).toEqual([WETH9[1], token0, token1, token3]) + expect(result[0].outputAmount.currency).toEqual(token3) + expect(result[1].inputAmount.currency).toEqual(ETHER) + expect(result[1].swaps[0].route.path).toEqual([WETH9[1], token0, token3]) + expect(result[1].outputAmount.currency).toEqual(token3) + }) + + it('works for ETHER currency output', async () => { + const result = await MixedRouteTrade.bestTradeExactIn( + [pool_weth_0, pool_0_1, pair_0_3, pair_1_3], + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(100)), + ETHER + ) + expect(result).toHaveLength(2) + expect(result[0].inputAmount.currency).toEqual(token3) + expect(result[0].swaps[0].route.path).toEqual([token3, token0, WETH9[1]]) + expect(result[0].outputAmount.currency).toEqual(ETHER) + expect(result[1].inputAmount.currency).toEqual(token3) + expect(result[1].swaps[0].route.path).toEqual([token3, token1, token0, WETH9[1]]) + expect(result[1].outputAmount.currency).toEqual(ETHER) + }) + }) + + describe('#maximumAmountIn', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + beforeEach(async () => { + exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1, pair_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)), + TradeType.EXACT_INPUT + ) + }) + it('throws if less than 0', () => { + expect(() => exactIn.maximumAmountIn(new Percent(JSBI.BigInt(-1), JSBI.BigInt(100)))).toThrow( + 'SLIPPAGE_TOLERANCE' + ) + }) + it('returns exact if 0', () => { + expect(exactIn.maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100)))).toEqual(exactIn.inputAmount) + }) + it('returns exact if nonzero', () => { + expect( + exactIn + .maximumAmountIn(new Percent(JSBI.BigInt(0), JSBI.BigInt(100))) + .equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + ).toBeTruthy() + expect( + exactIn + .maximumAmountIn(new Percent(JSBI.BigInt(5), JSBI.BigInt(100))) + .equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + ).toBeTruthy() + expect( + exactIn + .maximumAmountIn(new Percent(JSBI.BigInt(200), JSBI.BigInt(100))) + .equalTo(CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100))) + ).toBeTruthy() + }) + }) + }) + + describe('#minimumAmountOut', () => { + describe('tradeType = EXACT_INPUT', () => { + let exactIn: MixedRouteTrade + const large_pair_0_1 = new Pair( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100000)), + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100000)) + ) + beforeEach( + async () => + (exactIn = await MixedRouteTrade.fromRoute( + new MixedRouteSDK([large_pair_0_1, pool_1_2], token0, token2), + CurrencyAmount.fromRawAmount(token0, 10000), + TradeType.EXACT_INPUT + )) + ) + + it('throws if less than 0', () => { + expect(() => exactIn.minimumAmountOut(new Percent(JSBI.BigInt(-1), 100))).toThrow('SLIPPAGE_TOLERANCE') + }) + + it('returns exact if 0', () => { + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), 10000))).toEqual(exactIn.outputAmount) + }) + + it('returns exact if nonzero', () => { + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(0), 100))).toEqual( + CurrencyAmount.fromRawAmount(token2, 7004) + ) + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(5), 100))).toEqual( + CurrencyAmount.fromRawAmount(token2, 6670) + ) + expect(exactIn.minimumAmountOut(new Percent(JSBI.BigInt(200), 100))).toEqual( + CurrencyAmount.fromRawAmount(token2, 2334) + ) + }) + }) + }) + }) +}) diff --git a/src/entities/mixedRoute/trade.ts b/src/entities/mixedRoute/trade.ts new file mode 100644 index 0000000..c356eaa --- /dev/null +++ b/src/entities/mixedRoute/trade.ts @@ -0,0 +1,500 @@ +import { Currency, Fraction, Percent, Price, sortedInsert, CurrencyAmount, TradeType, Token } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' +import { BestTradeOptions, Pool } from '@uniswap/v3-sdk' +import invariant from 'tiny-invariant' +import { ONE, ZERO } from '../../constants' +import { MixedRouteSDK } from './route' + +/** + * Trades comparator, an extension of the input output comparator that also considers other dimensions of the trade in ranking them + * @template TInput The input token, either Ether or an ERC-20 + * @template TOutput The output token, either Ether or an ERC-20 + * @template TTradeType The trade type, either exact input or exact output + * @param a The first trade to compare + * @param b The second trade to compare + * @returns A sorted ordering for two neighboring elements in a trade array + */ +export function tradeComparator( + a: MixedRouteTrade, + b: MixedRouteTrade +) { + // must have same input and output token for comparison + invariant(a.inputAmount.currency.equals(b.inputAmount.currency), 'INPUT_CURRENCY') + invariant(a.outputAmount.currency.equals(b.outputAmount.currency), 'OUTPUT_CURRENCY') + if (a.outputAmount.equalTo(b.outputAmount)) { + if (a.inputAmount.equalTo(b.inputAmount)) { + // consider the number of hops since each hop costs gas + const aHops = a.swaps.reduce((total, cur) => total + cur.route.path.length, 0) + const bHops = b.swaps.reduce((total, cur) => total + cur.route.path.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. + * @notice This class is functionally the same as the `Trade` class in the `@uniswap/v3-sdk` package, aside from typing and some input validation. + * @template TInput The input token, either Ether or an ERC-20 + * @template TOutput The output token, either Ether or an ERC-20 + * @template TTradeType The trade type, either exact input or exact output + */ +export class MixedRouteTrade { + /** + * @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(): MixedRouteSDK { + 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: MixedRouteSDK + 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 a trade by simulating swaps through the given route + * @template TInput The input token, either Ether or an ERC-20. + * @template TOutput The output token, either Ether or an ERC-20. + * @template TTradeType The type of the trade, either exact in or exact out. + * @param route route to swap through + * @param amount the amount specified, either input or output, depending on tradeType + * @param tradeType whether the trade is an exact input or exact output swap + * @returns The route + */ + public static async fromRoute( + route: MixedRouteSDK, + amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount, + tradeType: TTradeType + ): Promise> { + const amounts: CurrencyAmount[] = new Array(route.path.length) + let inputAmount: CurrencyAmount + let outputAmount: CurrencyAmount + + invariant(tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE') + + invariant(amount.currency.equals(route.input), 'INPUT') + amounts[0] = amount.wrapped + for (let i = 0; i < route.path.length - 1; i++) { + const pool = route.pools[i] + const [outputAmount] = await pool.getOutputAmount(amounts[i]) + amounts[i + 1] = outputAmount + } + inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator) + outputAmount = CurrencyAmount.fromFractionalAmount( + route.output, + amounts[amounts.length - 1].numerator, + amounts[amounts.length - 1].denominator + ) + + return new MixedRouteTrade({ + routes: [{ inputAmount, outputAmount, route }], + tradeType, + }) + } + + /** + * Constructs a trade from routes by simulating swaps + * + * @template TInput The input token, either Ether or an ERC-20. + * @template TOutput The output token, either Ether or an ERC-20. + * @template TTradeType The type of the trade, either exact in or exact out. + * @param routes the routes to swap through and how much of the amount should be routed through each + * @param tradeType whether the trade is an exact input or exact output swap + * @returns The trade + */ + public static async fromRoutes( + routes: { + amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount + route: MixedRouteSDK + }[], + tradeType: TTradeType + ): Promise> { + const populatedRoutes: { + route: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] = [] + + invariant(tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE') + + for (const { route, amount } of routes) { + const amounts: CurrencyAmount[] = new Array(route.path.length) + let inputAmount: CurrencyAmount + let outputAmount: CurrencyAmount + + invariant(amount.currency.equals(route.input), 'INPUT') + inputAmount = CurrencyAmount.fromFractionalAmount(route.input, amount.numerator, amount.denominator) + amounts[0] = CurrencyAmount.fromFractionalAmount(route.input.wrapped, amount.numerator, amount.denominator) + + for (let i = 0; i < route.path.length - 1; i++) { + const pool = route.pools[i] + const [outputAmount] = await pool.getOutputAmount(amounts[i]) + amounts[i + 1] = outputAmount + } + + outputAmount = CurrencyAmount.fromFractionalAmount( + route.output, + amounts[amounts.length - 1].numerator, + amounts[amounts.length - 1].denominator + ) + + populatedRoutes.push({ route, inputAmount, outputAmount }) + } + + return new MixedRouteTrade({ + routes: populatedRoutes, + tradeType, + }) + } + + /** + * Creates a trade without computing the result of swapping through the route. Useful when you have simulated the trade + * elsewhere and do not have any tick data + * @template TInput The input token, either Ether or an ERC-20 + * @template TOutput The output token, either Ether or an ERC-20 + * @template TTradeType The type of the trade, either exact in or exact out + * @param constructorArguments The arguments passed to the trade constructor + * @returns The unchecked trade + */ + public static createUncheckedTrade< + TInput extends Currency, + TOutput extends Currency, + TTradeType extends TradeType + >(constructorArguments: { + route: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + tradeType: TTradeType + }): MixedRouteTrade { + return new MixedRouteTrade({ + ...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 token, either Ether or an ERC-20 + * @template TOutput The output token, either Ether or an ERC-20 + * @template TTradeType The type of the trade, either exact in or exact out + * @param constructorArguments The arguments passed to the trade constructor + * @returns The unchecked trade + */ + public static createUncheckedTradeWithMultipleRoutes< + TInput extends Currency, + TOutput extends Currency, + TTradeType extends TradeType + >(constructorArguments: { + routes: { + route: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] + tradeType: TTradeType + }): MixedRouteTrade { + return new MixedRouteTrade(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: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] + tradeType: TTradeType + }) { + const inputCurrency = routes[0].inputAmount.currency + const outputCurrency = routes[0].outputAmount.currency + invariant( + routes.every(({ route }) => inputCurrency.wrapped.equals(route.input.wrapped)), + 'INPUT_CURRENCY_MATCH' + ) + invariant( + routes.every(({ route }) => outputCurrency.wrapped.equals(route.output.wrapped)), + 'OUTPUT_CURRENCY_MATCH' + ) + + const numPools = routes.map(({ route }) => route.pools.length).reduce((total, cur) => total + cur, 0) + const poolAddressSet = new Set() + for (const { route } of routes) { + for (const pool of route.pools) { + pool instanceof Pool + ? poolAddressSet.add(Pool.getAddress(pool.token0, pool.token1, pool.fee)) + : poolAddressSet.add(Pair.getAddress(pool.token0, pool.token1)) + } + } + + invariant(numPools == poolAddressSet.size, 'POOLS_DUPLICATED') + + invariant(tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE') + + 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') + /// does not support exactOutput, as enforced in the constructor + const slippageAdjustedAmountOut = new Fraction(ONE) + .add(slippageTolerance) + .invert() + .multiply(amountOut.quotient).quotient + return CurrencyAmount.fromRawAmount(amountOut.currency, slippageAdjustedAmountOut) + } + + /** + * Get the maximum amount in that can be spent via this trade for the given slippage tolerance + * @param slippageTolerance The tolerance of unfavorable slippage from the execution price of this trade + * @returns The amount in + */ + public maximumAmountIn(slippageTolerance: Percent, amountIn = this.inputAmount): CurrencyAmount { + invariant(!slippageTolerance.lessThan(ZERO), 'SLIPPAGE_TOLERANCE') + return amountIn + /// does not support exactOutput + } + + /** + * 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 token + * amount to an output token, 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 | Pair)[], + currencyAmountIn: CurrencyAmount, + currencyOut: TOutput, + { maxNumResults = 3, maxHops = 3 }: BestTradeOptions = {}, + // used in recursion. + currentPools: (Pool | Pair)[] = [], + nextAmountIn: CurrencyAmount = currencyAmountIn, + bestTrades: MixedRouteTrade[] = [] + ): Promise[]> { + invariant(pools.length > 0, 'POOLS') + invariant(maxHops > 0, 'MAX_HOPS') + invariant(currencyAmountIn === nextAmountIn || currentPools.length > 0, 'INVALID_RECURSION') + + const amountIn = nextAmountIn.wrapped + const tokenOut = currencyOut.wrapped + for (let i = 0; i < pools.length; i++) { + const pool = pools[i] + // pool irrelevant + if (!pool.token0.equals(amountIn.currency) && !pool.token1.equals(amountIn.currency)) continue + if (pool instanceof Pair) { + if ((pool as Pair).reserve0.equalTo(ZERO) || (pool as Pair).reserve1.equalTo(ZERO)) continue + } + + let amountOut: CurrencyAmount + try { + ;[amountOut] = await pool.getOutputAmount(amountIn) + } catch (error) { + // input too low + // @ts-ignore[2571] error is unknown + if (error.isInsufficientInputAmountError) { + continue + } + throw error + } + // we have arrived at the output token, so this is the final trade of one of the paths + if (amountOut.currency.isToken && amountOut.currency.equals(tokenOut)) { + sortedInsert( + bestTrades, + await MixedRouteTrade.fromRoute( + new MixedRouteSDK([...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 token as long as we have not exceeded maxHops + await MixedRouteTrade.bestTradeExactIn( + poolsExcludingThisPool, + currencyAmountIn, + currencyOut, + { + maxNumResults, + maxHops: maxHops - 1, + }, + [...currentPools, pool], + amountOut, + bestTrades + ) + } + } + + return bestTrades + } +} diff --git a/src/entities/protocol.ts b/src/entities/protocol.ts index 654d3d5..c8ddd01 100644 --- a/src/entities/protocol.ts +++ b/src/entities/protocol.ts @@ -1,4 +1,5 @@ export enum Protocol { V2 = 'V2', V3 = 'V3', + MIXED = 'MIXED', } diff --git a/src/entities/route.ts b/src/entities/route.ts index 362bd8a..0183c31 100644 --- a/src/entities/route.ts +++ b/src/entities/route.ts @@ -1,7 +1,10 @@ +// entities/route.ts + import { Route as V2RouteSDK, Pair } from '@uniswap/v2-sdk' import { Route as V3RouteSDK, Pool } from '@uniswap/v3-sdk' import { Protocol } from './protocol' import { Currency, Price, Token } from '@uniswap/sdk-core' +import { MixedRouteSDK } from './mixedRoute/route' export interface IRoute { protocol: Protocol @@ -40,3 +43,15 @@ export class RouteV3 this.path = v3Route.tokenPath } } + +// Mixed route wrapper +export class MixedRoute + extends MixedRouteSDK + implements IRoute +{ + public readonly protocol: Protocol = Protocol.MIXED + + constructor(mixedRoute: MixedRouteSDK) { + super(mixedRoute.pools, mixedRoute.input, mixedRoute.output) + } +} diff --git a/src/entities/trade.test.ts b/src/entities/trade.test.ts index 933b9cb..2d9a550 100644 --- a/src/entities/trade.test.ts +++ b/src/entities/trade.test.ts @@ -1,6 +1,6 @@ import { sqrt, Token, CurrencyAmount, TradeType, WETH9, Ether, Percent, Price } from '@uniswap/sdk-core' import JSBI from 'jsbi' -import { RouteV2, RouteV3 } from './route' +import { MixedRoute, RouteV2, RouteV3 } from './route' import { Trade } from './trade' import { Route as V3RouteSDK, @@ -12,6 +12,7 @@ import { encodeSqrtRatioX96, } from '@uniswap/v3-sdk' import { Pair, Route as V2RouteSDK } from '@uniswap/v2-sdk' +import { MixedRouteSDK } from './mixedRoute/route' describe('Trade', () => { const ETHER = Ether.onChain(1) @@ -19,6 +20,7 @@ describe('Trade', () => { const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'token0') const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1', 'token1') const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2', 'token2') + const token3 = new Token(1, '0x0000000000000000000000000000000000000004', 18, 't3', 'token3') function v2StylePool( reserve0: CurrencyAmount, @@ -64,6 +66,11 @@ describe('Trade', () => { CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(10000)) ) + const pool_0_3 = v2StylePool( + CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(10000)) + ) + const pair_0_1 = new Pair( CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(12000)), CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(12000)) @@ -76,6 +83,10 @@ describe('Trade', () => { CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10000)), CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(12000)) ) + const pair_2_3 = new Pair( + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(10000)), + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(10000)) + ) const pair_weth_0 = new Pair( CurrencyAmount.fromRawAmount(weth, JSBI.BigInt(10000)), @@ -142,6 +153,24 @@ describe('Trade', () => { expect(trade.tradeType).toEqual(TradeType.EXACT_OUTPUT) }) + it('can contain only a mixed route', async () => { + const routeOriginal = new MixedRouteSDK([pool_0_1], token0, token1) + const route = new MixedRoute(routeOriginal) + + const amount = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + const tradeType = TradeType.EXACT_INPUT + const expectedOut = await pool_0_1.getOutputAmount(amount) + + const trade = await Trade.fromRoute(route, amount, tradeType) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(token1) + expect(trade.inputAmount).toEqual(amount) + expect(trade.outputAmount).toEqual(expectedOut[0]) + expect(trade.swaps.length).toEqual(1) + expect(trade.routes.length).toEqual(1) + expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) + }) + it('can be constructed with ETHER as input for a V3 Route exact input swap', async () => { const routeOriginal = new V3RouteSDK([pool_weth_0], ETHER, token0) const route = new RouteV3(routeOriginal) @@ -226,6 +255,28 @@ describe('Trade', () => { expect(trade.outputAmount.currency).toEqual(ETHER) }) + it('can be constructed with ETHER as input for a Mixed Route exact input swap', async () => { + const routeOriginal = new MixedRouteSDK([pool_weth_0], ETHER, token0) + const route = new MixedRoute(routeOriginal) + const amount = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(10)) + + const trade = await Trade.fromRoute(route, amount, TradeType.EXACT_INPUT) + expect(trade.inputAmount.currency).toEqual(ETHER) + expect(trade.outputAmount.currency).toEqual(token0) + }) + + it('can be constructed with ETHER as output for a Mixed Route exact input swap', async () => { + const routeOriginal = new MixedRouteSDK([pool_weth_0], token0, ETHER) + const route = new MixedRoute(routeOriginal) + const amount = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + const expectedOut = await pool_weth_0.getOutputAmount(amount) + const trade = await Trade.fromRoute(route, amount, TradeType.EXACT_INPUT) + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(ETHER) + expect(trade.inputAmount).toEqual(amount) + expect(trade.outputAmount.wrapped).toEqual(expectedOut[0]) + }) + it('throws if input currency does not match for V2 Route', async () => { const routeOriginal = new V2RouteSDK([pair_weth_2], token2, ETHER) const route = new RouteV2(routeOriginal) @@ -241,6 +292,7 @@ describe('Trade', () => { await expect(Trade.fromRoute(route, amount, TradeType.EXACT_OUTPUT)).rejects.toThrow('OUTPUT') }) + it('throws if input currency does not match for V3 route', async () => { const routeOriginal = new V3RouteSDK([pool_0_1], token0, token1) const route = new RouteV3(routeOriginal) @@ -259,6 +311,16 @@ describe('Trade', () => { const tradeType = TradeType.EXACT_OUTPUT await expect(Trade.fromRoute(route, amount, tradeType)).rejects.toThrow('OUTPUT') }) + + it('throws if input currency does not match for Mixed route', async () => { + const routeOriginal = new MixedRouteSDK([pool_0_1], token0, token1) + const route = new MixedRoute(routeOriginal) + + const amount = CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(1000)) + const tradeType = TradeType.EXACT_INPUT + + await expect(Trade.fromRoute(route, amount, tradeType)).rejects.toThrow('INPUT') + }) }) describe('#fromRoutes', () => { @@ -294,6 +356,93 @@ describe('Trade', () => { expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) }) + it('can contain a v2, a v3, and a mixed route', async () => { + const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_1_2], token0, token2) + const routev2 = new RouteV2(routeOriginalV2) + const amountv2 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + + const routeOriginalV3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const routev3 = new RouteV3(routeOriginalV3) + const amountv3 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + const mixedRouteOriginal = new MixedRouteSDK([pool_weth_0, pair_weth_2], token0, token2) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + const amountIn = amountv2.add(amountv3).add(amountMixedRoute) + + const outv2 = pair_1_2.getOutputAmount(pair_0_1.getOutputAmount(amountv2)[0])[0] + const out1v3 = await pool_0_1.getOutputAmount(amountv3) + const out2v3 = await pool_1_2.getOutputAmount(out1v3[0]) + const out1mixed = await pool_weth_0.getOutputAmount(amountMixedRoute) + const out2mixed = pair_weth_2.getOutputAmount(out1mixed[0])[0] + + const expectedOut = outv2.add(out2v3[0]).add(out2mixed) + + const trade = await Trade.fromRoutes( + [{ routev2, amount: amountv2 }], + [{ routev3, amount: amountv3 }], + TradeType.EXACT_INPUT, + [{ mixedRoute, amount: amountMixedRoute }] + ) + + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(token2) + expect(trade.inputAmount).toEqual(amountIn) + expect(trade.outputAmount).toEqual(expectedOut) + expect(trade.swaps.length).toEqual(3) + expect(trade.routes.length).toEqual(3) + expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) + }) + + it('can contain multiple v2, v3, and mixed routes', async () => { + const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_1_2], token0, token2) + const routev2 = new RouteV2(routeOriginalV2) + const amountv2 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + + const routeOriginalV3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const routev3 = new RouteV3(routeOriginalV3) + const amountv3 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + const mixedRouteOriginal = new MixedRouteSDK([pool_weth_0, pair_weth_2], token0, token2) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + const mixedRoute2Original = new MixedRouteSDK([pool_0_3, pair_2_3], token0, token2) + const mixedRoute2 = new MixedRoute(mixedRoute2Original) + const amountMixedRoute2 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + const amountIn = amountv2.add(amountv3).add(amountMixedRoute).add(amountMixedRoute2) + + const outv2 = pair_1_2.getOutputAmount(pair_0_1.getOutputAmount(amountv2)[0])[0] + const out1v3 = await pool_0_1.getOutputAmount(amountv3) + const out2v3 = await pool_1_2.getOutputAmount(out1v3[0]) + const out1mixed = await pool_weth_0.getOutputAmount(amountMixedRoute) + const out2mixed = pair_weth_2.getOutputAmount(out1mixed[0])[0] + const out1mixed2 = await pool_0_3.getOutputAmount(amountMixedRoute2) + const out2mixed2 = pair_2_3.getOutputAmount(out1mixed2[0])[0] + + const expectedOut = outv2.add(out2v3[0]).add(out2mixed).add(out2mixed2) + + const trade = await Trade.fromRoutes( + [{ routev2, amount: amountv2 }], + [{ routev3, amount: amountv3 }], + TradeType.EXACT_INPUT, + [ + { mixedRoute, amount: amountMixedRoute }, + { mixedRoute: mixedRoute2, amount: amountMixedRoute2 }, + ] + ) + + expect(trade.inputAmount.currency).toEqual(token0) + expect(trade.outputAmount.currency).toEqual(token2) + expect(trade.inputAmount).toEqual(amountIn) + expect(trade.outputAmount).toEqual(expectedOut) + expect(trade.swaps.length).toEqual(4) + expect(trade.routes.length).toEqual(4) + expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) + }) + it('can contain muliptle v2 and v3 routes', async () => { const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_1_2], token0, token2) const routev2 = new RouteV2(routeOriginalV2) @@ -358,16 +507,21 @@ describe('Trade', () => { const routev3 = new RouteV3(routeOriginalV3) const amountv3 = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(1000)) + const mixedRouteOriginal = new MixedRouteSDK([pool_weth_2, pair_1_2], ETHER, token1) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(1000)) + const trade = await Trade.fromRoutes( [{ routev2, amount: amountv2 }], [{ routev3, amount: amountv3 }], - TradeType.EXACT_INPUT + TradeType.EXACT_INPUT, + [{ mixedRoute, amount: amountMixedRoute }] ) expect(trade.inputAmount.currency).toEqual(ETHER) expect(trade.outputAmount.currency).toEqual(token1) - expect(trade.swaps.length).toEqual(2) - expect(trade.routes.length).toEqual(2) + expect(trade.swaps.length).toEqual(3) + expect(trade.routes.length).toEqual(3) expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) }) @@ -393,7 +547,7 @@ describe('Trade', () => { expect(trade.tradeType).toEqual(TradeType.EXACT_OUTPUT) }) - it('can be constructed with ETHER as ouput for exact output swap', async () => { + it('can be constructed with ETHER as output for exact output swap', async () => { const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_weth_0], token1, ETHER) const routev2 = new RouteV2(routeOriginalV2) const amountv2 = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(100)) @@ -415,7 +569,7 @@ describe('Trade', () => { expect(trade.tradeType).toEqual(TradeType.EXACT_OUTPUT) }) - it('can be constructed with ETHER as ouput for exact input swap', async () => { + it('can be constructed with ETHER as output for exact input swap', async () => { const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_weth_0], token1, ETHER) const routev2 = new RouteV2(routeOriginalV2) const amountv2 = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)) @@ -424,16 +578,21 @@ describe('Trade', () => { const routev3 = new RouteV3(routeOriginalV3) const amountv3 = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)) + const mixedRouteOriginal = new MixedRouteSDK([pair_1_2, pool_weth_2], token1, ETHER) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)) + const trade = await Trade.fromRoutes( [{ routev2, amount: amountv2 }], [{ routev3, amount: amountv3 }], - TradeType.EXACT_INPUT + TradeType.EXACT_INPUT, + [{ mixedRoute, amount: amountMixedRoute }] ) expect(trade.inputAmount.currency).toEqual(token1) expect(trade.outputAmount.currency).toEqual(ETHER) - expect(trade.swaps.length).toEqual(2) - expect(trade.routes.length).toEqual(2) + expect(trade.swaps.length).toEqual(3) + expect(trade.routes.length).toEqual(3) expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT) }) @@ -488,6 +647,48 @@ describe('Trade', () => { ).rejects.toThrow('POOLS_DUPLICATED') }) + it('throws if pools are re-used between mixed routes and v2 routes', async () => { + const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_1_2], token0, token2) + const routev2 = new RouteV2(routeOriginalV2) + const amountv2 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + + const routeOriginalV3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const routev3 = new RouteV3(routeOriginalV3) + const amountv3 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + // mixed route which will use v2 pair again + const mixedRouteOriginal = new MixedRouteSDK([pair_0_1, pool_weth_1, pool_weth_2], token0, token2) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + await expect( + Trade.fromRoutes([{ routev2, amount: amountv2 }], [{ routev3, amount: amountv3 }], TradeType.EXACT_INPUT, [ + { mixedRoute, amount: amountMixedRoute }, + ]) + ).rejects.toThrow('POOLS_DUPLICATED') + }) + + it('throws if pools are re-used between mixed routes and v3 routes', async () => { + const routeOriginalV2 = new V2RouteSDK([pair_0_1, pair_1_2], token0, token2) + const routev2 = new RouteV2(routeOriginalV2) + const amountv2 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + + const routeOriginalV3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const routev3 = new RouteV3(routeOriginalV3) + const amountv3 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + // mixed route which will use v3 pair again + const mixedRouteOriginal = new MixedRouteSDK([pool_0_1, pair_weth_1, pool_weth_2], token0, token2) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + await expect( + Trade.fromRoutes([{ routev2, amount: amountv2 }], [{ routev3, amount: amountv3 }], TradeType.EXACT_INPUT, [ + { mixedRoute, amount: amountMixedRoute }, + ]) + ).rejects.toThrow('POOLS_DUPLICATED') + }) + it('throws if routes have different inputs', async () => { const routeOriginalV2 = new V2RouteSDK([pair_1_2], token1, token2) const routev2 = new RouteV2(routeOriginalV2) @@ -502,6 +703,22 @@ describe('Trade', () => { ).rejects.toThrow('INPUT_CURRENCY_MATCH') }) + it('throws if routes have different inputs mixedRoute', async () => { + const routeOriginalV2 = new V2RouteSDK([pair_1_2], token1, token2) + const routev2 = new RouteV2(routeOriginalV2) + const amountv2 = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)) + + const mixedRouteOriginal = new MixedRouteSDK([pair_0_1, pool_1_2], token0, token2) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + await expect( + Trade.fromRoutes([{ routev2, amount: amountv2 }], [], TradeType.EXACT_INPUT, [ + { mixedRoute, amount: amountMixedRoute }, + ]) + ).rejects.toThrow('INPUT_CURRENCY_MATCH') + }) + it('throws if routes have different outputs', async () => { const routeOriginalV2 = new V2RouteSDK([pair_0_1], token0, token1) const routev2 = new RouteV2(routeOriginalV2) @@ -515,12 +732,48 @@ describe('Trade', () => { Trade.fromRoutes([{ routev2, amount: amountv2 }], [{ routev3, amount: amountv3 }], TradeType.EXACT_INPUT) ).rejects.toThrow('OUTPUT_CURRENCY_MATCH') }) + + it('throws if routes have different outputs mixedRoutes', async () => { + const routeOriginalV3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const routev3 = new RouteV3(routeOriginalV3) + const amountv3 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + const mixedRouteOriginal = new MixedRouteSDK([pair_0_1, pool_weth_1], token0, weth) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixedRoute = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(1000)) + + await expect( + Trade.fromRoutes([], [{ routev3, amount: amountv3 }], TradeType.EXACT_INPUT, [ + { mixedRoute, amount: amountMixedRoute }, + ]) + ).rejects.toThrow('OUTPUT_CURRENCY_MATCH') + }) + + it('throws if trade is created with EXACT_OUTPUT and contains mixedRoutes', async () => { + const routeOriginalV2 = new V2RouteSDK([pair_weth_0, pair_0_1], ETHER, token1) + const routev2 = new RouteV2(routeOriginalV2) + const amountv2 = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)) + + const mixedRouteOriginal = new MixedRouteSDK([pool_weth_0, pool_0_1], ETHER, token1) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const mixedRouteAmount = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)) + + await expect( + Trade.fromRoutes([{ routev2, amount: amountv2 }], [], TradeType.EXACT_OUTPUT, [ + { mixedRoute, amount: mixedRouteAmount }, + ]) + ).rejects.toThrow('TRADE_TYPE') + }) }) + describe('#worstExecutionPrice', () => { describe('tradeType = EXACT_INPUT', () => { const routev3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) const route2v3 = new V3RouteSDK([pool_0_2], token0, token2) + const mixedRoute = new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2) + const mixedRoute2 = new MixedRouteSDK([pool_0_2], token0, token2) + const inputAmount = CurrencyAmount.fromRawAmount(token0, 100) const outputAmount = CurrencyAmount.fromRawAmount(token2, 69) const tradeType = TradeType.EXACT_INPUT @@ -531,6 +784,13 @@ describe('Trade', () => { tradeType, }) + const exactInMixed = new Trade({ + v2Routes: [], + v3Routes: [], + tradeType, + mixedRoutes: [{ mixedRoute, inputAmount, outputAmount }], + }) + const exactInMultiRoute = new Trade({ v2Routes: [], v3Routes: [ @@ -548,23 +808,56 @@ describe('Trade', () => { tradeType: TradeType.EXACT_INPUT, }) + const exactInMultiMixedRoute = new Trade({ + v2Routes: [], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + mixedRoutes: [ + { + mixedRoute, + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 35), + }, + { + mixedRoute: mixedRoute2, + inputAmount: CurrencyAmount.fromRawAmount(token0, 50), + outputAmount: CurrencyAmount.fromRawAmount(token2, 34), + }, + ], + }) + it('throws if less than 0', () => { expect(() => exactInV3.worstExecutionPrice(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') + expect(() => exactInMixed.worstExecutionPrice(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') }) it('returns exact if 0', () => { expect(exactInV3.worstExecutionPrice(new Percent(0, 100))).toEqual(exactInV3.executionPrice) + expect(exactInMixed.worstExecutionPrice(new Percent(0, 100))).toEqual(exactInV3.executionPrice) }) it('returns exact if nonzero', () => { expect(exactInV3.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) expect(exactInV3.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) expect(exactInV3.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 100, 23)) + expect(exactInMixed.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) + expect(exactInMixed.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) + expect(exactInMixed.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 100, 23)) }) it('returns exact if nonzero with multiple routes', () => { expect(exactInMultiRoute.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 100, 69)) expect(exactInMultiRoute.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 100, 65)) expect(exactInMultiRoute.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 100, 23)) + expect(exactInMultiMixedRoute.worstExecutionPrice(new Percent(0, 100))).toEqual( + new Price(token0, token2, 100, 69) + ) + expect(exactInMultiMixedRoute.worstExecutionPrice(new Percent(5, 100))).toEqual( + new Price(token0, token2, 100, 65) + ) + expect(exactInMultiMixedRoute.worstExecutionPrice(new Percent(200, 100))).toEqual( + new Price(token0, token2, 100, 23) + ) }) }) + describe('tradeType = EXACT_OUTPUT', () => { const routev3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) const route2v3 = new V3RouteSDK([pool_0_2], token0, token2) @@ -627,9 +920,11 @@ describe('Trade', () => { ).toBeTruthy() }) }) + describe('worst execution price across v2 and v3 trades exact input', () => { const routev3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) const routev2 = new V2RouteSDK([pair_0_2], token0, token2) + const mixedRoute = new MixedRouteSDK([pool_weth_0, pair_weth_2], token0, token2) const exactIn = new Trade({ v2Routes: [ { @@ -646,6 +941,46 @@ describe('Trade', () => { }, ], tradeType: TradeType.EXACT_INPUT, + mixedRoutes: [ + { + mixedRoute, + inputAmount: CurrencyAmount.fromRawAmount(token0, 94), + outputAmount: CurrencyAmount.fromRawAmount(token2, 50), + }, + ], + }) + it('throws if less than 0', () => { + expect(() => exactIn.worstExecutionPrice(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') + }) + it('returns exact if 0', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(exactIn.executionPrice) + }) + it('returns exact if nonzero', () => { + expect(exactIn.worstExecutionPrice(new Percent(0, 100))).toEqual(new Price(token0, token2, 350, 250)) + expect(exactIn.worstExecutionPrice(new Percent(5, 100))).toEqual(new Price(token0, token2, 350, 238)) + expect(exactIn.worstExecutionPrice(new Percent(200, 100))).toEqual(new Price(token0, token2, 350, 83)) + }) + }) + + describe('worst execution price across only mixedRoute trades exact input', () => { + const mixedRoute = new MixedRouteSDK([pool_weth_0, pair_weth_2], token0, token2) + const mixedRoute2 = new MixedRouteSDK([pair_0_1, pool_weth_1, pool_weth_2], token0, token2) + const exactIn = new Trade({ + v2Routes: [], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + mixedRoutes: [ + { + mixedRoute, + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 100), + }, + { + mixedRoute: mixedRoute2, + inputAmount: CurrencyAmount.fromRawAmount(token0, 156), + outputAmount: CurrencyAmount.fromRawAmount(token2, 100), + }, + ], }) it('throws if less than 0', () => { expect(() => exactIn.worstExecutionPrice(new Percent(-1, 100))).toThrow('SLIPPAGE_TOLERANCE') @@ -698,6 +1033,7 @@ describe('Trade', () => { describe('tradeType = EXACT_INPUT', () => { const routev3 = new V3RouteSDK([pool_0_1], token0, token1) const routev2 = new V2RouteSDK([pair_0_1], token0, token1) + const mixedRoute = new MixedRouteSDK([pair_0_2, pool_1_2], token0, token1) const inputAmount = CurrencyAmount.fromRawAmount(token0, 100) const outputAmount = CurrencyAmount.fromRawAmount(token1, 100) @@ -707,6 +1043,7 @@ describe('Trade', () => { v2Routes: [{ routev2, inputAmount, outputAmount }], v3Routes: [{ routev3, inputAmount, outputAmount }], tradeType, + mixedRoutes: [{ mixedRoute, inputAmount, outputAmount }], }) it('throws if less than 0', () => { @@ -719,12 +1056,13 @@ describe('Trade', () => { it('returns exact if nonzero', () => { expect(trade.minimumAmountOut(new Percent(JSBI.BigInt(5), 100))).toEqual( - CurrencyAmount.fromRawAmount(token1, 190) + CurrencyAmount.fromRawAmount(token1, 285) // 300 * 0.95 ) expect(trade.minimumAmountOut(new Percent(JSBI.BigInt(200), 100))).toEqual( - CurrencyAmount.fromRawAmount(token1, 66) + CurrencyAmount.fromRawAmount(token1, 100) ) }) + describe('tradeType = EXACT_OUTPUT', () => { const routev3 = new V3RouteSDK([pool_0_1], token0, token1) const routev2 = new V2RouteSDK([pair_0_1], token0, token1) @@ -760,6 +1098,7 @@ describe('Trade', () => { describe('tradeType = EXACT_INPUT', () => { const routev3 = new V3RouteSDK([pool_0_1], token0, token1) const routev2 = new V2RouteSDK([pair_0_1], token0, token1) + const mixedRoute = new MixedRouteSDK([pair_0_2, pool_1_2], token0, token1) const inputAmount = CurrencyAmount.fromRawAmount(token0, 100) const outputAmount = CurrencyAmount.fromRawAmount(token1, 100) @@ -769,6 +1108,7 @@ describe('Trade', () => { v2Routes: [{ routev2, inputAmount, outputAmount }], v3Routes: [{ routev3, inputAmount, outputAmount }], tradeType, + mixedRoutes: [{ mixedRoute, inputAmount, outputAmount }], }) it('throws if less than 0', () => { @@ -781,9 +1121,10 @@ describe('Trade', () => { it('returns exact if nonzero', () => { expect(trade.maximumAmountIn(new Percent(JSBI.BigInt(5), 100))).toEqual( - CurrencyAmount.fromRawAmount(token0, 200) + CurrencyAmount.fromRawAmount(token0, 300) ) }) + describe('tradeType = EXACT_OUTPUT', () => { const routev3 = new V3RouteSDK([pool_0_1], token0, token1) const routev2 = new V2RouteSDK([pair_0_1], token0, token1) @@ -821,6 +1162,7 @@ describe('Trade', () => { describe('#priceImpact', () => { describe('tradeType = EXACT_INPUT', () => { const routev3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) + const mixedRoute = new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2) const trade = new Trade({ v2Routes: [], @@ -834,13 +1176,29 @@ describe('Trade', () => { tradeType: TradeType.EXACT_INPUT, }) + const mixedTrade = new Trade({ + v2Routes: [], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + mixedRoutes: [ + { + mixedRoute, + inputAmount: CurrencyAmount.fromRawAmount(token0, 100), + outputAmount: CurrencyAmount.fromRawAmount(token2, 69), + }, + ], + }) + it('is cached', () => { expect(trade.priceImpact === trade.priceImpact).toStrictEqual(true) + expect(mixedTrade.priceImpact.equalTo(trade.priceImpact)).toBe(true) }) it('is correct', () => { expect(trade.priceImpact.toSignificant(3)).toEqual('17.2') + expect(mixedTrade.priceImpact.toSignificant(3)).toEqual(trade.priceImpact.toSignificant(3)) }) }) + describe('tradeType = EXACT_OUTPUT', () => { const routev3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2) const exactOut = new Trade({ @@ -874,23 +1232,30 @@ describe('Trade', () => { const routev3 = new RouteV3(routeOriginalV3) const amountv3 = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + const mixedRouteOriginal = new MixedRouteSDK([pair_weth_0, pool_weth_1], token0, token1) + const mixedRoute = new MixedRoute(mixedRouteOriginal) + const amountMixed = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + const expectedOutV3 = await pool_0_1.getOutputAmount(amountv3) - const expectedOut = expectedOutV3[0].add(pair_0_1.getOutputAmount(amountv2)[0]) + const expectedOutMixed = await pool_weth_1.getOutputAmount((await pair_weth_0.getOutputAmount(amountMixed))[0]) + const expectedOut = expectedOutV3[0].add(pair_0_1.getOutputAmount(amountv2)[0]).add(expectedOutMixed[0]) const trade = await Trade.fromRoutes( [{ routev2, amount: amountv2 }], [{ routev3, amount: amountv3 }], - TradeType.EXACT_INPUT + TradeType.EXACT_INPUT, + [{ mixedRoute, amount: amountMixed }] ) const expectedPrice = new Price( token0, token1, - CurrencyAmount.fromRawAmount(token0, 200).quotient, + CurrencyAmount.fromRawAmount(token0, 300).quotient, expectedOut.quotient ) expect(trade.executionPrice).toEqual(expectedPrice) }) - it('is correct for tradeType = EXACT_INPUT', async () => { + + it('is correct for tradeType = EXACT_OUTPUT', async () => { const routeOriginalV2 = new V2RouteSDK([pair_0_1], token0, token1) const routev2 = new RouteV2(routeOriginalV2) const amountv2 = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)) diff --git a/src/entities/trade.ts b/src/entities/trade.ts index abe9807..730f630 100644 --- a/src/entities/trade.ts +++ b/src/entities/trade.ts @@ -3,8 +3,9 @@ import { Pair, Route as V2RouteSDK, Trade as V2TradeSDK } from '@uniswap/v2-sdk' import { Pool, Route as V3RouteSDK, Trade as V3TradeSDK } from '@uniswap/v3-sdk' import invariant from 'tiny-invariant' import { ONE, ZERO } from '../constants' -import { Protocol } from './protocol' -import { IRoute, RouteV2, RouteV3 } from './route' +import { MixedRouteSDK } from './mixedRoute/route' +import { MixedRouteTrade as MixedRouteTradeSDK } from './mixedRoute/trade' +import { IRoute, MixedRoute, RouteV2, RouteV3 } from './route' export class Trade { public readonly routes: IRoute[] @@ -27,6 +28,7 @@ export class Trade @@ -39,6 +41,11 @@ export class Trade }[] tradeType: TTradeType + mixedRoutes?: { + mixedRoute: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] }) { this.swaps = [] this.routes = [] @@ -62,6 +69,18 @@ export class Trade() for (const { route } of this.swaps) { for (const pool of route.pools) { - if (route.protocol == Protocol.V3) { + if (pool instanceof Pool) { poolAddressSet.add(Pool.getAddress(pool.token0, pool.token1, (pool as Pool).fee)) - } else { + } else if (pool instanceof Pair) { const pair = pool poolAddressSet.add(Pair.getAddress(pair.token0, pair.token1)) + } else { + throw new Error('Unexpected pool type in route when constructing trade object') } } } @@ -218,7 +239,11 @@ export class Trade amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount }[], - tradeType: TTradeType + tradeType: TTradeType, + mixedRoutes?: { + mixedRoute: MixedRouteSDK + amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount + }[] ): Promise> { const populatedV2Routes: { routev2: V2RouteSDK @@ -232,6 +257,12 @@ export class Trade }[] = [] + const populatedMixedRoutes: { + mixedRoute: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] = [] + for (const { routev2, amount } of v2Routes) { const v2Trade = new V2TradeSDK(routev2, amount, tradeType) const { inputAmount, outputAmount } = v2Trade @@ -254,15 +285,29 @@ export class Trade( - route: V2RouteSDK | V3RouteSDK, + route: V2RouteSDK | V3RouteSDK | MixedRouteSDK, amount: TTradeType extends TradeType.EXACT_INPUT ? CurrencyAmount : CurrencyAmount, tradeType: TTradeType ): Promise> { @@ -270,28 +315,40 @@ export class Trade inputAmount: CurrencyAmount outputAmount: CurrencyAmount - }[] + }[] = [] let v3Routes: { routev3: V3RouteSDK inputAmount: CurrencyAmount outputAmount: CurrencyAmount - }[] + }[] = [] + + let mixedRoutes: { + mixedRoute: MixedRouteSDK + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] = [] if (route instanceof V2RouteSDK) { const v2Trade = new V2TradeSDK(route, amount, tradeType) const { inputAmount, outputAmount } = v2Trade v2Routes = [{ routev2: route, inputAmount, outputAmount }] - v3Routes = [] - } else { + } else if (route instanceof V3RouteSDK) { const v3Trade = await V3TradeSDK.fromRoute(route, amount, tradeType) const { inputAmount, outputAmount } = v3Trade v3Routes = [{ routev3: route, inputAmount, outputAmount }] - v2Routes = [] + } else if (route instanceof MixedRouteSDK) { + const mixedRouteTrade = await MixedRouteTradeSDK.fromRoute(route, amount, tradeType) + const { inputAmount, outputAmount } = mixedRouteTrade + mixedRoutes = [{ mixedRoute: route, inputAmount, outputAmount }] + } else { + throw new Error('Invalid route type') } + return new Trade({ v2Routes, v3Routes, + mixedRoutes, tradeType, }) } diff --git a/src/index.ts b/src/index.ts index 14b5666..234b404 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,3 +6,6 @@ export * from './swapRouter' export * from './entities/trade' export * from './entities/protocol' export * from './entities/route' +export * from './entities/mixedRoute/route' +export * from './utils/encodeMixedRouteToPath' +export * from './utils' diff --git a/src/swapRouter.test.ts b/src/swapRouter.test.ts index f87d1a4..de38efe 100644 --- a/src/swapRouter.test.ts +++ b/src/swapRouter.test.ts @@ -14,6 +14,8 @@ import { import JSBI from 'jsbi' import { SwapRouter, Trade } from '.' import { ApprovalTypes } from './approveAndCall' +import { MixedRouteSDK } from './entities/mixedRoute/route' +import { MixedRouteTrade } from './entities/mixedRoute/trade' describe('SwapRouter', () => { const ETHER = Ether.onChain(1) @@ -21,6 +23,7 @@ describe('SwapRouter', () => { const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'token0') const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1', 'token1') + const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2', 'token2') const feeAmount = FeeAmount.MEDIUM const sqrtRatioX96 = encodeSqrtRatioX96(1, 1) @@ -52,9 +55,12 @@ describe('SwapRouter', () => { const pool_0_1 = makePool(token0, token1, liquidity) const pair_0_1 = makePair(token0, token1, liquidity) + const pair_1_2 = makePair(token1, token2, liquidity) const pool_1_WETH = makePool(token1, WETH, liquidity) const pair_1_WETH = makePair(token1, WETH, liquidity) + const pair_2_WETH = makePair(token2, WETH, liquidity) + const pool_2_WETH = makePool(token2, WETH, liquidity) const slippageTolerance = new Percent(1, 100) const recipient = '0x0000000000000000000000000000000000000003' @@ -261,6 +267,180 @@ describe('SwapRouter', () => { }) }) + describe('Mixed Route', () => { + describe('single-hop exact input (v2 + v3) backwards compatible', () => { + describe('different trade configurations result in identical calldata', () => { + const expectedCalldata = + '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e4472b43f300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000062000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000061000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + + const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade1 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) + + it('generates the same calldata', async () => { + const trades = [v2Trade, await v3Trade] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + + const mixedRouteTrades = [await mixedRouteTrade1, await mixedRouteTrade2] + const { calldata: mixedRouteCalldata, value: mixedRouteValue } = SwapRouter.swapCallParameters( + mixedRouteTrades, + { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + } + ) + expect(mixedRouteCalldata).toEqual(expectedCalldata) + expect(mixedRouteValue).toBe('0x00') + }) + + it('meta-trade', async () => { + const trades = await Trade.fromRoutes( + [ + { + routev2: v2Trade.route, + amount: amountIn, + }, + ], + [ + { + routev3: (await v3Trade).swaps[0].route, + amount: amountIn, + }, + ], + TradeType.EXACT_INPUT + ) + + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + + const mixedRouteTrades = await Trade.fromRoutes([], [], TradeType.EXACT_INPUT, [ + { + mixedRoute: (await mixedRouteTrade1).swaps[0].route, + amount: amountIn, + }, + { + mixedRoute: (await mixedRouteTrade2).swaps[0].route, + amount: amountIn, + }, + ]) + + const { calldata: mixedRouteCalldata, value: mixedRouteValue } = SwapRouter.swapCallParameters( + mixedRouteTrades, + { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + } + ) + expect(mixedRouteCalldata).toEqual(expectedCalldata) + expect(mixedRouteValue).toBe('0x00') + }) + }) + }) + + describe('multi-hop exact input (mixed route) backwards compatible', () => { + describe('different trade configurations result in identical calldata', () => { + /// calldata verified and taken from existing test + const expectedCalldata = + '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000104472b43f30000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000124b858183f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000005f00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + + const mixedRouteTrade1 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_WETH], token0, WETH), + amountIn, + TradeType.EXACT_INPUT + ) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1, pool_1_WETH], token0, WETH), + amountIn, + TradeType.EXACT_INPUT + ) + + it('single mixedRoute trade', async () => { + const trades = [await mixedRouteTrade1, await mixedRouteTrade2] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) + + it('meta-trade', async () => { + const trades = await Trade.fromRoutes([], [], TradeType.EXACT_INPUT, [ + { + mixedRoute: (await mixedRouteTrade1).swaps[0].route, + amount: amountIn, + }, + { + mixedRoute: (await mixedRouteTrade2).swaps[0].route, + amount: amountIn, + }, + ]) + + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) + }) + }) + + describe('mixed route trades with routes with consecutive pools/pairs', () => { + /// manually verified calldata + const expectedCalldata = + '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000124b858183f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e4472b43f30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104472b43f300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104b858183f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f000000000000000000000000000000000000000000000000000000000000002b0000000000000000000000000000000000000003000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) + const mixedRouteTrade1 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1, pool_1_WETH, pair_2_WETH], token0, WETH), + amountIn, + TradeType.EXACT_INPUT + ) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_2, pool_2_WETH], token0, WETH), + amountIn, + TradeType.EXACT_INPUT + ) + + it('generates correct calldata', async () => { + const trades = [await mixedRouteTrade1, await mixedRouteTrade2] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) + }) + }) + describe('ETH input', () => { describe('single-hop exact input (v2 + v3)', () => { describe('different trade configurations result in identical calldata', () => { @@ -270,6 +450,12 @@ describe('SwapRouter', () => { const v2Trade = V2Trade.exactIn(new V2Route([pair_1_WETH], ETHER, token1), amountIn) const v3Trade = V3Trade.fromRoute(new V3Route([pool_1_WETH], ETHER, token1), amountIn, TradeType.EXACT_INPUT) + /// mixedRouteTrade mirrors the V3Trade + const mixedRouteTrade = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_1_WETH], ETHER, token1), + amountIn, + TradeType.EXACT_INPUT + ) it('array of trades', async () => { const trades = [v2Trade, await v3Trade] @@ -282,6 +468,17 @@ describe('SwapRouter', () => { expect(value).toBe('0xc8') }) + it('mixedRoute produces the same calldata when swapped in', async () => { + const trades = [v2Trade, await mixedRouteTrade] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0xc8') + }) + it('meta-trade', async () => { const trades = await Trade.fromRoutes( [ @@ -307,6 +504,33 @@ describe('SwapRouter', () => { expect(calldata).toEqual(expectedCalldata) expect(value).toBe('0xc8') }) + + it('meta-trade with mixedRoute', async () => { + const trades = await Trade.fromRoutes( + [ + { + routev2: v2Trade.route, + amount: amountIn, + }, + ], + [], + TradeType.EXACT_INPUT, + [ + { + mixedRoute: (await mixedRouteTrade).swaps[0].route, + amount: amountIn, + }, + ] + ) + + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0xc8') + }) }) }) @@ -369,6 +593,12 @@ describe('SwapRouter', () => { const amountIn = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(100)) const v2Trade = V2Trade.exactIn(new V2Route([pair_1_WETH, pair_0_1], ETHER, token0), amountIn) + /// mixedRouteTrade mirrors V2Trade + const mixedRouteTrade = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_1_WETH, pair_0_1], ETHER, token0), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute( new V3Route([pool_1_WETH, pool_0_1], ETHER, token0), amountIn, @@ -386,6 +616,17 @@ describe('SwapRouter', () => { expect(value).toBe('0xc8') }) + it('mixedRoutes in array produce same calldata', async () => { + const trades = [await mixedRouteTrade, await v3Trade] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0xc8') + }) + it('meta-trade', async () => { const trades = await Trade.fromRoutes( [ @@ -411,6 +652,37 @@ describe('SwapRouter', () => { expect(calldata).toEqual(expectedCalldata) expect(value).toBe('0xc8') }) + + it('meta-trade with mixedRoutes produces calldata in different order but same content', async () => { + /// @dev since we order the calldata in the array in a particular way (v2, v3, mixedRoute) the ordering will be different, but the encoded swap data will be the same + /// Additionally, since we aren't sharing balances across trades order should not matter + const expectedCalldata = + '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000124b858183f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000005f0000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104472b43f300000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000061000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000' + const trades = await Trade.fromRoutes( + [], + [ + { + routev3: (await v3Trade).swaps[0].route, + amount: amountIn, + }, + ], + TradeType.EXACT_INPUT, + [ + { + mixedRoute: (await mixedRouteTrade).swaps[0].route, + amount: amountIn, + }, + ] + ) + + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0xc8') + }) }) }) @@ -530,6 +802,12 @@ describe('SwapRouter', () => { const v2Trade = V2Trade.exactIn(new V2Route([pair_1_WETH], token1, ETHER), amountIn) const v3Trade = V3Trade.fromRoute(new V3Route([pool_1_WETH], token1, ETHER), amountIn, TradeType.EXACT_INPUT) + /// mixedRoute mirrors v3Trade + const mixedRouteTrade = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_1_WETH], token1, ETHER), + amountIn, + TradeType.EXACT_INPUT + ) it('array of trades', async () => { const trades = [v2Trade, await v3Trade] @@ -542,6 +820,17 @@ describe('SwapRouter', () => { expect(value).toBe('0x00') }) + it('array of trades with mixedRoute produces same calldata', async () => { + const trades = [v2Trade, await mixedRouteTrade] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) + it('meta-trade', async () => { const trades = await Trade.fromRoutes( [ @@ -567,6 +856,33 @@ describe('SwapRouter', () => { expect(calldata).toEqual(expectedCalldata) expect(value).toBe('0x00') }) + + it('meta-trade with mixedRoute produces same calldata', async () => { + const trades = await Trade.fromRoutes( + [ + { + routev2: v2Trade.route, + amount: amountIn, + }, + ], + [], + TradeType.EXACT_INPUT, + [ + { + mixedRoute: (await mixedRouteTrade).swaps[0].route, + amount: amountIn, + }, + ] + ) + + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) }) }) @@ -629,6 +945,12 @@ describe('SwapRouter', () => { const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1, pair_1_WETH], token0, ETHER), amountIn) + /// mixedRouteTrade mirrors v2Trade + const mixedRouteTrade = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_WETH], token0, ETHER), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute( new V3Route([pool_0_1, pool_1_WETH], token0, ETHER), amountIn, @@ -646,6 +968,17 @@ describe('SwapRouter', () => { expect(value).toBe('0x00') }) + it('array of trades with mixedRoute produces same calldata', async () => { + const trades = [await mixedRouteTrade, await v3Trade] + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) + it('meta-trade', async () => { const trades = await Trade.fromRoutes( [ @@ -671,17 +1004,46 @@ describe('SwapRouter', () => { expect(calldata).toEqual(expectedCalldata) expect(value).toBe('0x00') }) - }) - }) - describe('multi-hop exact output (v2 + v3)', () => { - describe('different trade configurations result in identical calldata', () => { - const expectedCalldata = - '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010442712a670000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006700000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012409b81346000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000690000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000002000bb8000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004449404b7c00000000000000000000000000000000000000000000000000000000000000c8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000' - const amountOut = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(100)) + it('meta-trade with mixedRoute produces same calldata but in different order', async () => { + const expectedCalldata = + '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000124b858183f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000005f00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104472b43f30000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004449404b7c00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000' + const trades = await Trade.fromRoutes( + [], + [ + { + routev3: (await v3Trade).swaps[0].route, + amount: amountIn, + }, + ], + TradeType.EXACT_INPUT, + [ + { + mixedRoute: (await mixedRouteTrade).swaps[0].route, + amount: amountIn, + }, + ] + ) - const v2Trade = V2Trade.exactOut(new V2Route([pair_0_1, pair_1_WETH], token0, ETHER), amountOut) - const v3Trade = V3Trade.fromRoute( + const { calldata, value } = SwapRouter.swapCallParameters(trades, { + slippageTolerance, + recipient, + deadlineOrPreviousBlockhash: deadline, + }) + expect(calldata).toEqual(expectedCalldata) + expect(value).toBe('0x00') + }) + }) + }) + + describe('multi-hop exact output (v2 + v3)', () => { + describe('different trade configurations result in identical calldata', () => { + const expectedCalldata = + '0x5ae401dc000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010442712a670000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006700000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012409b81346000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000690000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000002000bb8000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004449404b7c00000000000000000000000000000000000000000000000000000000000000c8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000' + const amountOut = CurrencyAmount.fromRawAmount(ETHER, JSBI.BigInt(100)) + + const v2Trade = V2Trade.exactOut(new V2Route([pair_0_1, pair_1_WETH], token0, ETHER), amountOut) + const v3Trade = V3Trade.fromRoute( new V3Route([pool_0_1, pool_1_WETH], token0, ETHER), amountOut, TradeType.EXACT_OUTPUT @@ -733,7 +1095,20 @@ describe('SwapRouter', () => { '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000056000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c89c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010411ed56c9000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb8ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc4000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + /// mirrors V2Trade + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + /// mixedRoute mirrors v3Trade + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) + const position = new Position({ pool: pool_0_1, tickLower: -60, @@ -758,6 +1133,32 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process when v2 + mixedRoute', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process when mixedRoute + v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('multi-hop trades', () => { @@ -767,11 +1168,24 @@ describe('SwapRouter', () => { const pool_0_WETH = makePool(token0, WETH, liquidity) const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1, pair_1_WETH], token0, WETH), amountIn) + /// mirrors V2Trade + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1, pair_1_WETH], token0, WETH), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute( new V3Route([pool_0_1, pool_1_WETH], token0, WETH), amountIn, TradeType.EXACT_INPUT ) + /// mixedRoute mirrors v3Trade + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1, pool_1_WETH], token0, WETH), + amountIn, + TradeType.EXACT_INPUT + ) + const position = new Position({ pool: pool_0_WETH, tickLower: -60, @@ -796,6 +1210,45 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('encodes the entire swap process for a multi route trade with v2, mixedRoute', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('encodes the entire swap process for a multi route trade with mixedRoute, v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('encodes the entire swap process for a multi route trade with mixedRoute, mixedRoute', async () => { + const trades = [await mixedRouteTrade2, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('adding liquidity to an existing position', () => { @@ -803,7 +1256,18 @@ describe('SwapRouter', () => { '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000004200000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000058000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c89c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4f100b20500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) + const position = new Position({ pool: pool_0_1, tickLower: -60, @@ -828,6 +1292,32 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process, v2, mixed', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process, mixed, v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.NOT_REQUIRED, + ApprovalTypes.NOT_REQUIRED + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('when MAX tokens must be approved to the NFT Manager', () => { @@ -835,7 +1325,19 @@ describe('SwapRouter', () => { '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c89c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024571ac8b00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024571ac8b000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4f100b20500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) + const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) + const position = new Position({ pool: pool_0_1, tickLower: -60, @@ -860,6 +1362,45 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process, v2, mixed', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX, + ApprovalTypes.MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process, mixed, v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX, + ApprovalTypes.MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process, mixed, mixed', async () => { + const trades = [await mixedRouteTrade2, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX, + ApprovalTypes.MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('when MAX_MINUS_ONE tokens must be approved to the NFT Manager', () => { @@ -867,7 +1408,17 @@ describe('SwapRouter', () => { '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c89c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024cab372ce0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024cab372ce00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4f100b20500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const position = new Position({ pool: pool_0_1, tickLower: -60, @@ -892,6 +1443,45 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process, v2, mixed', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX_MINUS_ONE, + ApprovalTypes.MAX_MINUS_ONE + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process, mixed, v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX_MINUS_ONE, + ApprovalTypes.MAX_MINUS_ONE + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process, mixed, mixed', async () => { + const trades = [await mixedRouteTrade2, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX_MINUS_ONE, + ApprovalTypes.MAX_MINUS_ONE + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('when ZERO_THEN_MAX tokens must be approved to the NFT Manager', () => { @@ -899,7 +1489,17 @@ describe('SwapRouter', () => { '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c89c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024639d71a90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024639d71a900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4f100b20500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const position = new Position({ pool: pool_0_1, tickLower: -60, @@ -924,6 +1524,45 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process v2, mixed', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.ZERO_THEN_MAX, + ApprovalTypes.ZERO_THEN_MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process mixed, v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.ZERO_THEN_MAX, + ApprovalTypes.ZERO_THEN_MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process mixed, mixed', async () => { + const trades = [await mixedRouteTrade2, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.ZERO_THEN_MAX, + ApprovalTypes.ZERO_THEN_MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('when ZERO_THEN_MAX_MINUS_ONE tokens must be approved to the NFT Manager', () => { @@ -931,7 +1570,17 @@ describe('SwapRouter', () => { '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000005200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000e4472b43f3000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000032c89c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044f2d5d56b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000032c8ad000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024ab3fdd500000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024ab3fdd5000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4f100b20500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e90a182f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' const amountIn = CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(10)) const v2Trade = V2Trade.exactIn(new V2Route([pair_0_1], token0, token1), amountIn) + const mixedRouteTrade2 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pair_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const v3Trade = V3Trade.fromRoute(new V3Route([pool_0_1], token0, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_0_1], token0, token1), + amountIn, + TradeType.EXACT_INPUT + ) const position = new Position({ pool: pool_0_1, tickLower: -60, @@ -956,6 +1605,45 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process v2, mixed', async () => { + const trades = [v2Trade, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.ZERO_THEN_MAX_MINUS_ONE, + ApprovalTypes.ZERO_THEN_MAX_MINUS_ONE + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process mixed, v3', async () => { + const trades = [await mixedRouteTrade2, await v3Trade] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.ZERO_THEN_MAX_MINUS_ONE, + ApprovalTypes.ZERO_THEN_MAX_MINUS_ONE + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) + + it('correctly encodes the entire swap process mixed, mixed', async () => { + const trades = [await mixedRouteTrade2, await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.ZERO_THEN_MAX_MINUS_ONE, + ApprovalTypes.ZERO_THEN_MAX_MINUS_ONE + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('when input is native', () => { @@ -965,6 +1653,11 @@ describe('SwapRouter', () => { const ETH = Ether.onChain(1) const amountIn = CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(10)) const v3Trade = V3Trade.fromRoute(new V3Route([pool_1_WETH], ETH, token1), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_1_WETH], ETH, token1), + amountIn, + TradeType.EXACT_INPUT + ) const position = new Position({ pool: pool_1_WETH, tickLower: -60, @@ -989,6 +1682,19 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process when is a mixedRoute', async () => { + const trades = [await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX, + ApprovalTypes.MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) describe('when output is native', () => { @@ -997,6 +1703,11 @@ describe('SwapRouter', () => { const ETH = Ether.onChain(1) const amountIn = CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(10)) const v3Trade = V3Trade.fromRoute(new V3Route([pool_1_WETH], token1, ETH), amountIn, TradeType.EXACT_INPUT) + const mixedRouteTrade3 = MixedRouteTrade.fromRoute( + new MixedRouteSDK([pool_1_WETH], token1, ETH), + amountIn, + TradeType.EXACT_INPUT + ) const position = new Position({ pool: pool_1_WETH, tickLower: -60, @@ -1021,6 +1732,19 @@ describe('SwapRouter', () => { ) expect(methodParameters.calldata).toEqual(expectedCalldata) }) + + it('correctly encodes the entire swap process when is mixedRoute', async () => { + const trades = [await mixedRouteTrade3] + const methodParameters = SwapRouter.swapAndAddCallParameters( + trades, + { slippageTolerance }, + position, + addLiquidityOptions, + ApprovalTypes.MAX, + ApprovalTypes.MAX + ) + expect(methodParameters.calldata).toEqual(expectedCalldata) + }) }) }) }) diff --git a/src/swapRouter.ts b/src/swapRouter.ts index 09f765b..76cfc8a 100644 --- a/src/swapRouter.ts +++ b/src/swapRouter.ts @@ -8,6 +8,7 @@ import { MethodParameters, Payments, PermitOptions, + Pool, Position, SelfPermit, toHex, @@ -19,9 +20,13 @@ import { ADDRESS_THIS, MSG_SENDER } from './constants' import { ApproveAndCall, ApprovalTypes, CondensedAddLiquidityOptions } from './approveAndCall' import { Trade } from './entities/trade' import { Protocol } from './entities/protocol' -import { RouteV2, RouteV3 } from './entities/route' +import { MixedRoute, RouteV2, RouteV3 } from './entities/route' import { MulticallExtended, Validation } from './multicallExtended' import { PaymentsExtended } from './paymentsExtended' +import { MixedRouteTrade } from './entities/mixedRoute/trade' +import { encodeMixedRouteToPath } from './utils/encodeMixedRouteToPath' +import { MixedRouteSDK } from './entities/mixedRoute/route' +import { partitionMixedRouteByProtocol, getOutputOfPools } from './utils' const ZERO = JSBI.BigInt(0) const REFUND_ETH_PRICE_IMPACT_THRESHOLD = new Percent(JSBI.BigInt(50), JSBI.BigInt(100)) @@ -67,7 +72,12 @@ type AnyTradeType = | Trade | V2Trade | V3Trade - | (V2Trade | V3Trade)[] + | MixedRouteTrade + | ( + | V2Trade + | V3Trade + | MixedRouteTrade + )[] /** * Represents the Uniswap V2 + V3 SwapRouter02, and has static methods for helping execute trades. @@ -80,6 +90,14 @@ export abstract class SwapRouter { */ private constructor() {} + /** + * @notice Generates the calldata for a Swap with a V2 Route. + * @param trade The V2Trade to encode. + * @param options SwapOptions to use for the trade. + * @param routerMustCustody Flag for whether funds should be sent to the router + * @param performAggregatedSlippageCheck Flag for whether we want to perform an aggregated slippage check + * @returns A string array of calldatas for the trade. + */ private static encodeV2Swap( trade: V2Trade, options: SwapOptions, @@ -107,6 +125,14 @@ export abstract class SwapRouter { } } + /** + * @notice Generates the calldata for a Swap with a V3 Route. + * @param trade The V3Trade to encode. + * @param options SwapOptions to use for the trade. + * @param routerMustCustody Flag for whether funds should be sent to the router + * @param performAggregatedSlippageCheck Flag for whether we want to perform an aggregated slippage check + * @returns A string array of calldatas for the trade. + */ private static encodeV3Swap( trade: V3Trade, options: SwapOptions, @@ -182,13 +208,129 @@ export abstract class SwapRouter { return calldatas } + /** + * @notice Generates the calldata for a MixedRouteSwap. Since single hop routes are not MixedRoutes, we will instead generate + * them via the existing encodeV3Swap and encodeV2Swap methods. + * @param trade The MixedRouteTrade to encode. + * @param options SwapOptions to use for the trade. + * @param routerMustCustody Flag for whether funds should be sent to the router + * @param performAggregatedSlippageCheck Flag for whether we want to perform an aggregated slippage check + * @returns A string array of calldatas for the trade. + */ + private static encodeMixedRouteSwap( + trade: MixedRouteTrade, + options: SwapOptions, + routerMustCustody: boolean, + performAggregatedSlippageCheck: boolean + ): string[] { + const calldatas: string[] = [] + + invariant(trade.tradeType === TradeType.EXACT_INPUT, 'TRADE_TYPE') + + for (const { route, inputAmount, outputAmount } of trade.swaps) { + const amountIn: string = toHex(trade.maximumAmountIn(options.slippageTolerance, inputAmount).quotient) + const amountOut: string = toHex(trade.minimumAmountOut(options.slippageTolerance, outputAmount).quotient) + + // flag for whether the trade is single hop or not + const singleHop = route.pools.length === 1 + + const recipient = routerMustCustody + ? ADDRESS_THIS + : typeof options.recipient === 'undefined' + ? MSG_SENDER + : validateAndParseAddress(options.recipient) + + const mixedRouteIsAllV3 = (route: MixedRouteSDK) => { + return route.pools.every((pool) => pool instanceof Pool) + } + + if (singleHop) { + /// For single hop, since it isn't really a mixedRoute, we'll just mimic behavior of V3 or V2 + /// We don't use encodeV3Swap() or encodeV2Swap() because casting the trade to a V3Trade or V2Trade is overcomplex + if (mixedRouteIsAllV3(route)) { + const exactInputSingleParams = { + tokenIn: route.path[0].address, + tokenOut: route.path[1].address, + fee: (route.pools as Pool[])[0].fee, + recipient, + amountIn, + amountOutMinimum: performAggregatedSlippageCheck ? 0 : amountOut, + sqrtPriceLimitX96: 0, + } + + calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactInputSingle', [exactInputSingleParams])) + } else { + const path = route.path.map((token) => token.address) + + const exactInputParams = [amountIn, performAggregatedSlippageCheck ? 0 : amountOut, path, recipient] + + calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('swapExactTokensForTokens', exactInputParams)) + } + } else { + const sections = partitionMixedRouteByProtocol(route) + + const isLastSectionInRoute = (i: number) => { + return i === sections.length - 1 + } + + let outputToken + let inputToken = route.input.wrapped + + for (let i = 0; i < sections.length; i++) { + const section = sections[i] + /// Now, we get output of this section + outputToken = getOutputOfPools(section, inputToken) + + const newRouteOriginal = new MixedRouteSDK( + [...section], + section[0].token0.equals(inputToken) ? section[0].token0 : section[0].token1, + outputToken + ) + const newRoute = new MixedRoute(newRouteOriginal) + + /// Previous output is now input + inputToken = outputToken + + if (mixedRouteIsAllV3(newRoute)) { + const path: string = encodeMixedRouteToPath(newRoute) + const exactInputParams = { + path, + // By default router holds funds until the last swap, then it is sent to the recipient + // special case exists where we are unwrapping WETH output, in which case `routerMustCustody` is set to true + // and router still holds the funds. That logic bundled into how the value of `recipient` is calculated + recipient: isLastSectionInRoute(i) ? recipient : ADDRESS_THIS, + amountIn: i == 0 ? amountIn : 0, + amountOutMinimum: !isLastSectionInRoute(i) ? 0 : amountOut, + } + + calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactInput', [exactInputParams])) + } else { + const exactInputParams = [ + i == 0 ? amountIn : 0, // amountIn + !isLastSectionInRoute(i) ? 0 : amountOut, // amountOutMin + newRoute.path.map((token) => token.address), // path + isLastSectionInRoute(i) ? recipient : ADDRESS_THIS, // to + ] + + calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('swapExactTokensForTokens', exactInputParams)) + } + } + } + } + + return calldatas + } + private static encodeSwaps( trades: AnyTradeType, options: SwapOptions, isSwapAndAdd?: boolean ): { calldatas: string[] - sampleTrade: V2Trade | V3Trade + sampleTrade: + | V2Trade + | V3Trade + | MixedRouteTrade routerMustCustody: boolean inputIsNative: boolean outputIsNative: boolean @@ -196,18 +338,27 @@ export abstract class SwapRouter { minimumAmountOut: CurrencyAmount quoteAmountOut: CurrencyAmount } { - // If dealing with an instance of the aggregated Trade object, unbundle it to individual V2Trade and V3Trade objects. + // If dealing with an instance of the aggregated Trade object, unbundle it to individual trade objects. if (trades instanceof Trade) { invariant( - trades.swaps.every((swap) => swap.route.protocol == Protocol.V3 || swap.route.protocol == Protocol.V2), + trades.swaps.every( + (swap) => + swap.route.protocol == Protocol.V3 || + swap.route.protocol == Protocol.V2 || + swap.route.protocol == Protocol.MIXED + ), 'UNSUPPORTED_PROTOCOL' ) - let v2Andv3Trades: (V2Trade | V3Trade)[] = [] + let individualTrades: ( + | V2Trade + | V3Trade + | MixedRouteTrade + )[] = [] for (const { route, inputAmount, outputAmount } of trades.swaps) { if (route.protocol == Protocol.V2) { - v2Andv3Trades.push( + individualTrades.push( new V2Trade( route as RouteV2, trades.tradeType == TradeType.EXACT_INPUT ? inputAmount : outputAmount, @@ -215,7 +366,7 @@ export abstract class SwapRouter { ) ) } else if (route.protocol == Protocol.V3) { - v2Andv3Trades.push( + individualTrades.push( V3Trade.createUncheckedTrade({ route: route as RouteV3, inputAmount, @@ -223,9 +374,21 @@ export abstract class SwapRouter { tradeType: trades.tradeType, }) ) + } else if (route.protocol == Protocol.MIXED) { + individualTrades.push( + /// we can change the naming of this function on MixedRouteTrade if needed + MixedRouteTrade.createUncheckedTrade({ + route: route as MixedRoute, + inputAmount, + outputAmount, + tradeType: trades.tradeType, + }) + ) + } else { + throw new Error('UNSUPPORTED_TRADE_PROTOCOL') } } - trades = v2Andv3Trades + trades = individualTrades } if (!Array.isArray(trades)) { @@ -233,7 +396,8 @@ export abstract class SwapRouter { } const numberOfTrades = trades.reduce( - (numberOfTrades, trade) => numberOfTrades + (trade instanceof V3Trade ? trade.swaps.length : 1), + (numberOfTrades, trade) => + numberOfTrades + (trade instanceof V3Trade || trade instanceof MixedRouteTrade ? trade.swaps.length : 1), 0 ) @@ -279,7 +443,7 @@ export abstract class SwapRouter { for (const trade of trades) { if (trade instanceof V2Trade) { calldatas.push(SwapRouter.encodeV2Swap(trade, options, routerMustCustody, performAggregatedSlippageCheck)) - } else { + } else if (trade instanceof V3Trade) { for (const calldata of SwapRouter.encodeV3Swap( trade, options, @@ -288,6 +452,17 @@ export abstract class SwapRouter { )) { calldatas.push(calldata) } + } else if (trade instanceof MixedRouteTrade) { + for (const calldata of SwapRouter.encodeMixedRouteSwap( + trade, + options, + routerMustCustody, + performAggregatedSlippageCheck + )) { + calldatas.push(calldata) + } + } else { + throw new Error('Unsupported trade object') } } @@ -323,7 +498,7 @@ export abstract class SwapRouter { /** * Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade. - * @param trade to produce call parameters for + * @param trades to produce call parameters for * @param options options for the call parameters */ public static swapCallParameters( @@ -331,7 +506,12 @@ export abstract class SwapRouter { | Trade | V2Trade | V3Trade - | (V2Trade | V3Trade)[], + | MixedRouteTrade + | ( + | V2Trade + | V3Trade + | MixedRouteTrade + )[], options: SwapOptions ): MethodParameters { const { @@ -374,7 +554,7 @@ export abstract class SwapRouter { /** * Produces the on-chain method name to call and the hex encoded parameters to pass as arguments for a given trade. - * @param trade to produce call parameters for + * @param trades to produce call parameters for * @param options options for the call parameters */ public static swapAndAddCallParameters( @@ -485,6 +665,7 @@ export abstract class SwapRouter { | Trade | V2Trade | V3Trade + | MixedRouteTrade ): boolean { return !(trade instanceof V2Trade) && trade.priceImpact.greaterThan(REFUND_ETH_PRICE_IMPACT_THRESHOLD) } @@ -492,7 +673,10 @@ export abstract class SwapRouter { private static getPositionAmounts( position: Position, zeroForOne: boolean - ): { positionAmountIn: CurrencyAmount; positionAmountOut: CurrencyAmount } { + ): { + positionAmountIn: CurrencyAmount + positionAmountOut: CurrencyAmount + } { const { amount0, amount1 } = position.mintAmounts const currencyAmount0 = CurrencyAmount.fromRawAmount(position.pool.token0, amount0) const currencyAmount1 = CurrencyAmount.fromRawAmount(position.pool.token1, amount1) diff --git a/src/utils/encodeMixedRouteToPath.test.ts b/src/utils/encodeMixedRouteToPath.test.ts new file mode 100644 index 0000000..6008249 --- /dev/null +++ b/src/utils/encodeMixedRouteToPath.test.ts @@ -0,0 +1,139 @@ +import { CurrencyAmount, Ether, Token, WETH9 } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' +import { encodeSqrtRatioX96, FeeAmount, Pool } from '@uniswap/v3-sdk' +import { MixedRouteSDK } from '../entities/mixedRoute/route' +import { encodeMixedRouteToPath } from './encodeMixedRouteToPath' + +describe('#encodeMixedRouteToPath', () => { + const ETHER = Ether.onChain(1) + const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'token0') + const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1', 'token1') + const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2', 'token2') + + const weth = WETH9[1] + + const pool_0_1_medium = new Pool(token0, token1, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_1_2_low = new Pool(token1, token2, FeeAmount.LOW, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_0_weth = new Pool(token0, weth, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_1_weth = new Pool(token1, weth, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) + + const pair_0_1 = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(token1, '200')) + const pair_1_2 = new Pair(CurrencyAmount.fromRawAmount(token1, '150'), CurrencyAmount.fromRawAmount(token2, '150')) + const pair_0_weth = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(weth, '100')) + const pair_1_weth = new Pair(CurrencyAmount.fromRawAmount(token1, '175'), CurrencyAmount.fromRawAmount(weth, '100')) + const pair_2_weth = new Pair(CurrencyAmount.fromRawAmount(token2, '150'), CurrencyAmount.fromRawAmount(weth, '100')) + + const route_0_V3_1 = new MixedRouteSDK([pool_0_1_medium], token0, token1) + const route_0_V3_1_V3_2 = new MixedRouteSDK([pool_0_1_medium, pool_1_2_low], token0, token2) + const route_0_V3_weth = new MixedRouteSDK([pool_0_weth], token0, ETHER) + const route_0_V3_1_V3_weth = new MixedRouteSDK([pool_0_1_medium, pool_1_weth], token0, ETHER) + const route_weth_V3_0 = new MixedRouteSDK([pool_0_weth], ETHER, token0) + const route_weth_V3_0_V3_1 = new MixedRouteSDK([pool_0_weth, pool_0_1_medium], ETHER, token1) + + const route_0_V2_1 = new MixedRouteSDK([pair_0_1], token0, token1) + const route_0_V2_1_V2_2 = new MixedRouteSDK([pair_0_1, pair_1_2], token0, token2) + const route_weth_V2_0 = new MixedRouteSDK([pair_0_weth], ETHER, token0) + const route_weth_V2_0_V2_1 = new MixedRouteSDK([pair_0_weth, pair_0_1], ETHER, token1) + const route_0_V2_weth = new MixedRouteSDK([pair_0_weth], token0, ETHER) + const route_0_V2_1_V2_weth = new MixedRouteSDK([pair_0_1, pair_1_weth], token0, ETHER) + + const route_0_V3_1_V2_weth = new MixedRouteSDK([pool_0_1_medium, pair_1_weth], token0, ETHER) + const route_0_V3_weth_V2_1_V2_2 = new MixedRouteSDK([pool_0_weth, pair_1_weth, pair_1_2], token0, token2) + const route_0_V3_1_v3_weth_V2_2 = new MixedRouteSDK([pool_0_1_medium, pool_1_weth, pair_2_weth], token0, token2) + + describe('pure V3', () => { + it('packs them for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_0_V3_1)).toEqual( + '0x0000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002' + ) + }) + + it('packs them correctly for multihop exact input', () => { + expect(encodeMixedRouteToPath(route_0_V3_1_V3_2)).toEqual( + '0x0000000000000000000000000000000000000001000bb800000000000000000000000000000000000000020001f40000000000000000000000000000000000000003' + ) + }) + + it('wraps ether input for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_weth_V3_0)).toEqual( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000001' + ) + }) + + it('wraps ether input for exact input multihop', () => { + expect(encodeMixedRouteToPath(route_weth_V3_0_V3_1)).toEqual( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002' + ) + }) + + it('wraps ether output for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_0_V3_weth)).toEqual( + '0x0000000000000000000000000000000000000001000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ) + }) + + it('wraps ether output for exact input multihop', () => { + expect(encodeMixedRouteToPath(route_0_V3_1_V3_weth)).toEqual( + '0x0000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ) + }) + }) + + describe('pure V2', () => { + it('packs them for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_0_V2_1)).toEqual( + '0x00000000000000000000000000000000000000018000000000000000000000000000000000000000000002' + ) + }) + + it('packs them correctly for multihop exact input', () => { + expect(encodeMixedRouteToPath(route_0_V2_1_V2_2)).toEqual( + '0x000000000000000000000000000000000000000180000000000000000000000000000000000000000000028000000000000000000000000000000000000000000003' + ) + }) + + it('wraps ether input for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_weth_V2_0)).toEqual( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc28000000000000000000000000000000000000000000001' + ) + }) + + it('wraps ether input for exact input multihop', () => { + expect(encodeMixedRouteToPath(route_weth_V2_0_V2_1)).toEqual( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc280000000000000000000000000000000000000000000018000000000000000000000000000000000000000000002' + ) + }) + + it('wraps ether output for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_0_V2_weth)).toEqual( + '0x0000000000000000000000000000000000000001800000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ) + }) + + it('wraps ether output for exact input multihop', () => { + expect(encodeMixedRouteToPath(route_0_V2_1_V2_weth)).toEqual( + '0x00000000000000000000000000000000000000018000000000000000000000000000000000000000000002800000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ) + }) + }) + + describe('mixed route', () => { + it('packs them for exact input v3 -> v2 with wrapped ether output', () => { + expect(encodeMixedRouteToPath(route_0_V3_1_V2_weth)).toEqual( + '0x0000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002800000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ) + }) + + it('packs them for exact input v3 -> v2 -> v2', () => { + expect(encodeMixedRouteToPath(route_0_V3_weth_V2_1_V2_2)).toEqual( + '0x0000000000000000000000000000000000000001000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc280000000000000000000000000000000000000000000028000000000000000000000000000000000000000000003' + ) + }) + + it('packs them for exact input v3 -> v3 -> v2', () => { + expect(encodeMixedRouteToPath(route_0_V3_1_v3_weth_V2_2)).toEqual( + '0x0000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc28000000000000000000000000000000000000000000003' + ) + }) + }) +}) diff --git a/src/utils/encodeMixedRouteToPath.ts b/src/utils/encodeMixedRouteToPath.ts new file mode 100644 index 0000000..6d5e7a6 --- /dev/null +++ b/src/utils/encodeMixedRouteToPath.ts @@ -0,0 +1,42 @@ +import { pack } from '@ethersproject/solidity' +import { Currency, Token } from '@uniswap/sdk-core' +import { Pool } from '@uniswap/v3-sdk' +import { Pair } from '@uniswap/v2-sdk' +import { MixedRouteSDK } from '../entities/mixedRoute/route' +import { V2_FEE_PATH_PLACEHOLDER } from '../constants' + +/** + * Converts a route to a hex encoded path + * @notice only supports exactIn route encodings + * @param route the mixed path to convert to an encoded path + * @returns the exactIn encoded path + */ +export function encodeMixedRouteToPath(route: MixedRouteSDK): string { + const firstInputToken: Token = route.input.wrapped + + const { path, types } = route.pools.reduce( + ( + { inputToken, path, types }: { inputToken: Token; path: (string | number)[]; types: string[] }, + pool: Pool | Pair, + index + ): { inputToken: Token; path: (string | number)[]; types: string[] } => { + const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + if (index === 0) { + return { + inputToken: outputToken, + types: ['address', 'uint24', 'address'], + path: [inputToken.address, pool instanceof Pool ? pool.fee : V2_FEE_PATH_PLACEHOLDER, outputToken.address], + } + } else { + return { + inputToken: outputToken, + types: [...types, 'uint24', 'address'], + path: [...path, pool instanceof Pool ? pool.fee : V2_FEE_PATH_PLACEHOLDER, outputToken.address], + } + } + }, + { inputToken: firstInputToken, path: [], types: [] } + ) + + return pack(types, path) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..7cb08ab --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,52 @@ +import { Currency, Token } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' +import { Pool } from '@uniswap/v3-sdk' +import { MixedRouteSDK } from '../entities/mixedRoute/route' + +/** + * Utility function to return each consecutive section of Pools or Pairs in a MixedRoute + * @param route + * @returns a nested array of Pools or Pairs in the order of the route + */ +export const partitionMixedRouteByProtocol = (route: MixedRouteSDK): (Pool | Pair)[][] => { + let acc = [] + + let left = 0 + let right = 0 + while (right < route.pools.length) { + if ( + (route.pools[left] instanceof Pool && route.pools[right] instanceof Pair) || + (route.pools[left] instanceof Pair && route.pools[right] instanceof Pool) + ) { + acc.push(route.pools.slice(left, right)) + left = right + } + // seek forward with right pointer + right++ + if (right === route.pools.length) { + /// we reached the end, take the rest + acc.push(route.pools.slice(left, right)) + } + } + return acc +} + +/** + * Simple utility function to get the output of an array of Pools or Pairs + * @param pools + * @param firstInputToken + * @returns the output token of the last pool in the array + */ +export const getOutputOfPools = (pools: (Pool | Pair)[], firstInputToken: Token): Token => { + const { inputToken: outputToken } = pools.reduce( + ({ inputToken }, pool: Pool | Pair): { inputToken: Token } => { + if (!pool.involvesToken(inputToken)) throw new Error('PATH') + const outputToken: Token = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + return { + inputToken: outputToken, + } + }, + { inputToken: firstInputToken } + ) + return outputToken +} diff --git a/yarn.lock b/yarn.lock index 0436f81..15d00df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6696,4 +6696,4 @@ yargs@^15.3.1: string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^18.1.2" + yargs-parser "^18.1.2" \ No newline at end of file