From 9d1bdbb4137bdc9a3ba90c38f2856ad8b5c72e6c Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Thu, 15 Feb 2024 01:51:32 -0500 Subject: [PATCH] Add Quoter (#28) * Add route encoding to path and update dependencies Added a new utility function to encode routes to paths. This is useful when making swaps and trades on Uniswap's routes. Updated several dependencies to their latest versions, including the uniswap-sdk-core. Also created relevant tests for the new utility. * Add Quoter module A new module named Quoter has been introduced. This module provides functionality for determining on-chain method name and parameters for the appropriate function within QuoterV2. It has been integrated into the prelude for easier access throughout the codebase. * Add unit tests for trade input in quoter.rs Unit tests are added to verify the functionality of trade input in quoter.rs file. It covers both the single and multi-hop scenarios of exact input and output. It also tests different square root price limits and use of quoter version 2. All of these tests ensure that interaction with the Uniswap protocol works correctly. * Refactor code to use references instead of cloning The code has been refactored to pass routes and trade parameters by reference instead of cloning. This change helps improve efficiency by avoiding unnecessary data duplication. The changes touch on the `encode_route_to_path` function, the `quote_call_parameters` function and related tests, ensuring that each function and method now receives a reference to a route or trade, instead of a cloned object. --- Cargo.lock | 159 +++++++------- Cargo.toml | 4 +- src/abi.rs | 78 +++++++ src/lib.rs | 4 +- src/quoter.rs | 335 ++++++++++++++++++++++++++++++ src/utils/encode_route_to_path.rs | 257 +++++++++++++++++++++++ src/utils/mod.rs | 2 + 7 files changed, 766 insertions(+), 73 deletions(-) create mode 100644 src/quoter.rs create mode 100644 src/utils/encode_route_to_path.rs diff --git a/Cargo.lock b/Cargo.lock index 8b0f3de..16adb59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -117,9 +117,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anyhow" @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" dependencies = [ "serde", ] @@ -536,9 +536,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "num-traits", ] @@ -582,18 +582,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.18" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" dependencies = [ "anstyle", "clap_lex", @@ -601,9 +601,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "coins-bip32" @@ -659,9 +659,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5104de16b218eddf8e34ffe2f86f74bfa4e61e95a1b89732fccf6325efd0557" +checksum = "18d59688ad0945eaf6b84cb44fedbe93484c81b48970e98f09db8a22832d7961" dependencies = [ "cfg-if", "cpufeatures", @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -960,9 +960,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "elliptic-curve" @@ -1697,9 +1697,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" [[package]] name = "hex" @@ -1859,9 +1859,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown", @@ -1893,12 +1893,12 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", - "rustix", + "libc", "windows-sys 0.52.0", ] @@ -1928,18 +1928,18 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -2137,11 +2137,10 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] @@ -2159,9 +2158,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -2361,9 +2360,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.6" +version = "2.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" dependencies = [ "memchr", "thiserror", @@ -3531,13 +3530,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] @@ -3555,18 +3553,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", @@ -3720,14 +3718,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.1", + "toml_edit 0.22.5", ] [[package]] @@ -3747,7 +3745,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -3758,7 +3756,7 @@ checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -3766,12 +3764,23 @@ name = "toml_edit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e68c159e8f5ba8a28c4eb7b0c0c190d77bb479047ca713270048145a9ad28a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.0", ] [[package]] @@ -3906,9 +3915,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "uniswap-sdk-core" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b36075329d71604f738f4e1564cd44f0cf46480eeea2fa5c740b7a270898599" +checksum = "fb8c6dc4b6dd4653700903fd87becde38f949d8183f9435606ed714a6a7d920e" dependencies = [ "alloy-primitives", "bigdecimal", @@ -3919,12 +3928,13 @@ dependencies = [ "num-rational", "num-traits", "regex", + "syn 2.0.48", "thiserror", ] [[package]] name = "uniswap-v3-sdk" -version = "0.21.0" +version = "0.22.0" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -4053,9 +4063,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4063,9 +4073,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", @@ -4078,9 +4088,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -4090,9 +4100,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4100,9 +4110,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", @@ -4113,15 +4123,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -4298,9 +4308,18 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.37" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" +checksum = "6b1dbce9e90e5404c5a52ed82b1d13fc8cfbdad85033b6f57546ffd1265f8451" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index f774299..b368732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-v3-sdk" -version = "0.21.0" +version = "0.22.0" edition = "2021" authors = ["Shuhui Luo "] description = "Uniswap V3 SDK for Rust" @@ -30,7 +30,7 @@ regex = { version = "1.10", optional = true } ruint = "1.11" serde_json = { version = "1.0", optional = true } thiserror = "1.0" -uniswap-sdk-core = "0.12.0" +uniswap-sdk-core = "0.14.0" uniswap_v3_math = "0.4.1" [features] diff --git a/src/abi.rs b/src/abi.rs index 7d0249c..7a1e3c2 100644 --- a/src/abi.rs +++ b/src/abi.rs @@ -150,4 +150,82 @@ sol! { uint256 amountRequested ) external returns (uint256 reward); } + + interface IQuoter { + function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut); + + function quoteExactInputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + uint256 amountIn, + uint160 sqrtPriceLimitX96 + ) external returns (uint256 amountOut); + + function quoteExactOutput(bytes memory path, uint256 amountOut) external returns (uint256 amountIn); + + function quoteExactOutputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + uint256 amountOut, + uint160 sqrtPriceLimitX96 + ) external returns (uint256 amountIn); + } +} + +sol! { + interface IQuoterV2 { + function quoteExactInput(bytes memory path, uint256 amountIn) + external + returns ( + uint256 amountOut, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); + + struct QuoteExactInputSingleParams { + address tokenIn; + address tokenOut; + uint256 amountIn; + uint24 fee; + uint160 sqrtPriceLimitX96; + } + + function quoteExactInputSingle(QuoteExactInputSingleParams memory params) + external + returns ( + uint256 amountOut, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + + function quoteExactOutput(bytes memory path, uint256 amountOut) + external + returns ( + uint256 amountIn, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); + + struct QuoteExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint256 amount; + uint24 fee; + uint160 sqrtPriceLimitX96; + } + + function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) + external + returns ( + uint256 amountIn, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 9d07522..4c154bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod entities; pub mod multicall; pub mod nonfungible_position_manager; pub mod payments; +pub mod quoter; pub mod self_permit; pub mod staker; pub mod utils; @@ -19,7 +20,8 @@ pub mod extensions; pub mod prelude { pub use crate::{ abi::*, constants::*, entities::*, multicall::encode_multicall, - nonfungible_position_manager::*, payments::*, self_permit::*, staker::*, utils::*, + nonfungible_position_manager::*, payments::*, quoter::*, self_permit::*, staker::*, + utils::*, }; #[cfg(feature = "extensions")] diff --git a/src/quoter.rs b/src/quoter.rs new file mode 100644 index 0000000..84e31f0 --- /dev/null +++ b/src/quoter.rs @@ -0,0 +1,335 @@ +use crate::prelude::*; +use alloy_primitives::U256; +use alloy_sol_types::SolCall; +use uniswap_sdk_core::{constants::TradeType, prelude::*}; + +/// Optional arguments to send to the quoter. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct QuoteOptions { + /// The price limit for the trade. + sqrt_price_limit_x96: U256, + /// The quoter interface to use + use_quoter_v2: bool, +} + +/// Produces the on-chain method name of the appropriate function within QuoterV2, +/// and the relevant hex encoded parameters. +/// +/// ## Arguments +/// +/// * `route`: The swap route, a list of pools through which a swap can occur +/// * `amount`: The amount of the quote, either an amount in, or an amount out +/// * `trade_type`: The trade type, either exact input or exact output +/// * `options`: The optional params including price limit and Quoter contract switch +/// +pub fn quote_call_parameters( + route: &Route, + amount: CurrencyAmount, + trade_type: TradeType, + options: Option, +) -> MethodParameters { + let options = options.unwrap_or_default(); + let quote_amount = big_int_to_u256(amount.quotient()); + + let calldata = if route.pools.len() == 1 { + match trade_type { + TradeType::ExactInput => { + if options.use_quoter_v2 { + IQuoterV2::quoteExactInputSingleCall { + params: IQuoterV2::QuoteExactInputSingleParams { + tokenIn: route.token_path[0].address(), + tokenOut: route.token_path[1].address(), + amountIn: quote_amount, + fee: route.pools[0].fee as u32, + sqrtPriceLimitX96: options.sqrt_price_limit_x96, + }, + } + .abi_encode() + } else { + IQuoter::quoteExactInputSingleCall { + tokenIn: route.token_path[0].address(), + tokenOut: route.token_path[1].address(), + amountIn: quote_amount, + fee: route.pools[0].fee as u32, + sqrtPriceLimitX96: options.sqrt_price_limit_x96, + } + .abi_encode() + } + } + TradeType::ExactOutput => { + if options.use_quoter_v2 { + IQuoterV2::quoteExactOutputSingleCall { + params: IQuoterV2::QuoteExactOutputSingleParams { + tokenIn: route.token_path[0].address(), + tokenOut: route.token_path[1].address(), + amount: quote_amount, + fee: route.pools[0].fee as u32, + sqrtPriceLimitX96: options.sqrt_price_limit_x96, + }, + } + .abi_encode() + } else { + IQuoter::quoteExactOutputSingleCall { + tokenIn: route.token_path[0].address(), + tokenOut: route.token_path[1].address(), + amountOut: quote_amount, + fee: route.pools[0].fee as u32, + sqrtPriceLimitX96: options.sqrt_price_limit_x96, + } + .abi_encode() + } + } + } + } else { + assert!( + options.sqrt_price_limit_x96.is_zero(), + "MULTIHOP_PRICE_LIMIT" + ); + let path = encode_route_to_path(route, trade_type == TradeType::ExactOutput); + match trade_type { + TradeType::ExactInput => IQuoter::quoteExactInputCall { + path, + amountIn: quote_amount, + } + .abi_encode(), + TradeType::ExactOutput => IQuoter::quoteExactOutputCall { + path, + amountOut: quote_amount, + } + .abi_encode(), + } + }; + MethodParameters { + calldata, + value: U256::ZERO, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use once_cell::sync::Lazy; + use uniswap_sdk_core::token; + + static TOKEN0: Lazy = Lazy::new(|| { + token!( + 1, + "0x0000000000000000000000000000000000000001", + 18, + "t0", + "token0" + ) + }); + static TOKEN1: Lazy = Lazy::new(|| { + token!( + 1, + "0x0000000000000000000000000000000000000002", + 18, + "t1", + "token1" + ) + }); + static WETH: Lazy = Lazy::new(|| Ether::on_chain(1).wrapped()); + const FEE_AMOUNT: FeeAmount = FeeAmount::MEDIUM; + const SQRT_RATIO_X96: U256 = Q96; + const LIQUIDITY: u128 = 1_000_000; + + fn make_pool(token0: Token, token1: Token) -> Pool { + Pool::new_with_tick_data_provider( + token0, + token1, + FEE_AMOUNT, + SQRT_RATIO_X96, + LIQUIDITY, + TickListDataProvider::new( + vec![ + Tick::new( + nearest_usable_tick(MIN_TICK, FEE_AMOUNT.tick_spacing()), + LIQUIDITY, + LIQUIDITY as i128, + ), + Tick::new( + nearest_usable_tick(MAX_TICK, FEE_AMOUNT.tick_spacing()), + LIQUIDITY, + -(LIQUIDITY as i128), + ), + ], + FEE_AMOUNT.tick_spacing(), + ), + ) + .unwrap() + } + + static POOL_0_1: Lazy> = + Lazy::new(|| make_pool(TOKEN0.clone(), TOKEN1.clone())); + static POOL_1_WETH: Lazy> = + Lazy::new(|| make_pool(TOKEN1.clone(), WETH.clone())); + + mod single_trade_input { + use super::*; + use alloy_primitives::hex; + + #[test] + fn single_hop_exact_input() { + let mut trade = Trade::from_route( + Route::new(vec![POOL_0_1.clone()], TOKEN0.clone(), TOKEN1.clone()), + CurrencyAmount::from_raw_amount(TOKEN0.clone(), 100).unwrap(), + TradeType::ExactInput, + ) + .unwrap(); + let input_amount = trade.input_amount().unwrap(); + let params = + quote_call_parameters(&trade.swaps[0].route, input_amount, trade.trade_type, None); + assert_eq!( + params.calldata, + hex!("f7729d43000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000") + ); + assert_eq!(params.value, U256::ZERO); + } + + #[test] + fn single_hop_exact_output() { + let mut trade = Trade::from_route( + Route::new(vec![POOL_0_1.clone()], TOKEN0.clone(), TOKEN1.clone()), + CurrencyAmount::from_raw_amount(TOKEN1.clone(), 100).unwrap(), + TradeType::ExactOutput, + ) + .unwrap(); + let output_amount = trade.output_amount().unwrap(); + let params = + quote_call_parameters(&trade.swaps[0].route, output_amount, trade.trade_type, None); + assert_eq!( + params.calldata, + hex!("30d07f21000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000") + ); + assert_eq!(params.value, U256::ZERO); + } + + #[test] + fn multi_hop_exact_input() { + let mut trade = Trade::from_route( + Route::new( + vec![POOL_0_1.clone(), POOL_1_WETH.clone()], + TOKEN0.clone(), + WETH.clone(), + ), + CurrencyAmount::from_raw_amount(TOKEN0.clone(), 100).unwrap(), + TradeType::ExactInput, + ) + .unwrap(); + let params = quote_call_parameters( + &trade.route(), + trade.input_amount().unwrap(), + trade.trade_type, + None, + ); + assert_eq!( + params.calldata, + hex!("cdca17530000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000") + ); + assert_eq!(params.value, U256::ZERO); + } + + #[test] + fn multi_hop_exact_output() { + let mut trade = Trade::from_route( + Route::new( + vec![POOL_0_1.clone(), POOL_1_WETH.clone()], + TOKEN0.clone(), + WETH.clone(), + ), + CurrencyAmount::from_raw_amount(WETH.clone(), 100).unwrap(), + TradeType::ExactOutput, + ) + .unwrap(); + let params = quote_call_parameters( + &trade.route(), + trade.output_amount().unwrap(), + trade.trade_type, + None, + ); + assert_eq!( + params.calldata, + hex!("2f80bb1d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000") + ); + assert_eq!(params.value, U256::ZERO); + } + + #[test] + fn sqrt_price_limit_x96() { + let mut trade = Trade::from_route( + Route::new(vec![POOL_0_1.clone()], TOKEN0.clone(), TOKEN1.clone()), + CurrencyAmount::from_raw_amount(TOKEN0.clone(), 100).unwrap(), + TradeType::ExactInput, + ) + .unwrap(); + let params = quote_call_parameters( + &trade.route(), + trade.input_amount().unwrap(), + trade.trade_type, + Some(QuoteOptions { + sqrt_price_limit_x96: U256::from_limbs([0, 0, 1, 0]), + use_quoter_v2: false, + }), + ); + assert_eq!( + params.calldata, + hex!("f7729d43000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000100000000000000000000000000000000") + ); + assert_eq!(params.value, U256::ZERO); + } + } + + mod single_trade_input_using_quoter_v2 { + use super::*; + use alloy_primitives::hex; + + #[test] + fn single_hop_exact_input() { + let mut trade = Trade::from_route( + Route::new(vec![POOL_0_1.clone()], TOKEN0.clone(), TOKEN1.clone()), + CurrencyAmount::from_raw_amount(TOKEN0.clone(), 100).unwrap(), + TradeType::ExactInput, + ) + .unwrap(); + let input_amount = trade.input_amount().unwrap(); + let params = quote_call_parameters( + &trade.swaps[0].route, + input_amount, + trade.trade_type, + Some(QuoteOptions { + sqrt_price_limit_x96: U256::ZERO, + use_quoter_v2: true, + }), + ); + assert_eq!( + params.calldata, + hex!("c6a5026a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000000"), + ); + } + + #[test] + fn single_hop_exact_output() { + let mut trade = Trade::from_route( + Route::new(vec![POOL_0_1.clone()], TOKEN0.clone(), TOKEN1.clone()), + CurrencyAmount::from_raw_amount(TOKEN1.clone(), 100).unwrap(), + TradeType::ExactOutput, + ) + .unwrap(); + let output_amount = trade.output_amount().unwrap(); + let params = quote_call_parameters( + &trade.swaps[0].route, + output_amount, + trade.trade_type, + Some(QuoteOptions { + sqrt_price_limit_x96: U256::ZERO, + use_quoter_v2: true, + }), + ); + assert_eq!( + params.calldata, + hex!("bd21704a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000000"), + ); + } + } +} diff --git a/src/utils/encode_route_to_path.rs b/src/utils/encode_route_to_path.rs new file mode 100644 index 0000000..246a220 --- /dev/null +++ b/src/utils/encode_route_to_path.rs @@ -0,0 +1,257 @@ +use crate::prelude::Route; +use alloy_sol_types::{sol, SolType, SolValue}; +use uniswap_sdk_core::prelude::*; + +type TokenFee = sol!((address, uint24)); + +/// Converts a route to a hex encoded path +/// +/// ## Arguments +/// +/// * `route`: the v3 path to convert to an encoded path +/// * `exact_output`: whether the route should be encoded in reverse, for making exact output swaps +/// +pub fn encode_route_to_path( + route: &Route, + exact_output: bool, +) -> Vec { + let mut path: Vec = Vec::with_capacity(23 * route.pools.len() + 20); + if exact_output { + let mut output_token = &route.output.wrapped(); + for pool in route.pools.iter().rev() { + let leg = if pool.token0.equals(output_token) { + output_token = &pool.token1; + (pool.token0.address(), pool.fee as u32) + } else { + output_token = &pool.token0; + (pool.token1.address(), pool.fee as u32) + }; + path.extend(TokenFee::abi_encode_packed(&leg)); + } + path.extend(route.input.address().abi_encode_packed()); + } else { + let mut input_token = &route.input.wrapped(); + for pool in route.pools.iter() { + let leg = if pool.token0.equals(input_token) { + input_token = &pool.token1; + (pool.token0.address(), pool.fee as u32) + } else { + input_token = &pool.token0; + (pool.token1.address(), pool.fee as u32) + }; + path.extend(TokenFee::abi_encode_packed(&leg)); + } + path.extend(route.output.address().abi_encode_packed()); + } + path +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use alloy_primitives::hex; + use once_cell::sync::Lazy; + use uniswap_sdk_core::token; + + static ETHER: Lazy = Lazy::new(|| Ether::on_chain(1)); + static TOKEN0: Lazy = Lazy::new(|| { + token!( + 1, + "0x0000000000000000000000000000000000000001", + 18, + "t0", + "token0" + ) + }); + static TOKEN1: Lazy = Lazy::new(|| { + token!( + 1, + "0x0000000000000000000000000000000000000002", + 18, + "t1", + "token1" + ) + }); + static TOKEN2: Lazy = Lazy::new(|| { + token!( + 1, + "0x0000000000000000000000000000000000000003", + 18, + "t2", + "token2" + ) + }); + static WETH: Lazy = Lazy::new(|| ETHER.wrapped()); + + static POOL_0_1_MEDIUM: Lazy> = Lazy::new(|| { + Pool::new( + TOKEN0.clone(), + TOKEN1.clone(), + FeeAmount::MEDIUM, + encode_sqrt_ratio_x96(1, 1), + 0, + ) + .unwrap() + }); + static POOL_1_2_LOW: Lazy> = Lazy::new(|| { + Pool::new( + TOKEN1.clone(), + TOKEN2.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + 0, + ) + .unwrap() + }); + static POOL_0_WETH: Lazy> = Lazy::new(|| { + Pool::new( + TOKEN0.clone(), + WETH.clone(), + FeeAmount::MEDIUM, + encode_sqrt_ratio_x96(1, 1), + 0, + ) + .unwrap() + }); + static POOL_1_WETH: Lazy> = Lazy::new(|| { + Pool::new( + TOKEN1.clone(), + WETH.clone(), + FeeAmount::MEDIUM, + encode_sqrt_ratio_x96(1, 1), + 0, + ) + .unwrap() + }); + + static ROUTE_0_1: Lazy> = Lazy::new(|| { + Route::new( + vec![POOL_0_1_MEDIUM.clone()], + TOKEN0.clone(), + TOKEN1.clone(), + ) + }); + static ROUTE_0_1_2: Lazy> = Lazy::new(|| { + Route::new( + vec![POOL_0_1_MEDIUM.clone(), POOL_1_2_LOW.clone()], + TOKEN0.clone(), + TOKEN2.clone(), + ) + }); + + static ROUTE_0_WETH: Lazy> = + Lazy::new(|| Route::new(vec![POOL_0_WETH.clone()], TOKEN0.clone(), ETHER.clone())); + static ROUTE_0_1_WETH: Lazy> = Lazy::new(|| { + Route::new( + vec![POOL_0_1_MEDIUM.clone(), POOL_1_WETH.clone()], + TOKEN0.clone(), + ETHER.clone(), + ) + }); + static ROUTE_WETH_0: Lazy> = + Lazy::new(|| Route::new(vec![POOL_0_WETH.clone()], ETHER.clone(), TOKEN0.clone())); + static ROUTE_WETH_0_1: Lazy> = Lazy::new(|| { + Route::new( + vec![POOL_0_WETH.clone(), POOL_0_1_MEDIUM.clone()], + ETHER.clone(), + TOKEN1.clone(), + ) + }); + + #[test] + fn pack_them_for_exact_input_single_hop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_1, false), + hex!("0000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002") + ); + } + + #[test] + fn pack_them_for_exact_output_single_hop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_1, true), + hex!("0000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001") + ); + } + + #[test] + fn pack_them_for_exact_input_multihop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_1_2, false), + hex!("0000000000000000000000000000000000000001000bb800000000000000000000000000000000000000020001f40000000000000000000000000000000000000003") + ); + } + + #[test] + fn pack_them_for_exact_output_multihop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_1_2, true), + hex!("00000000000000000000000000000000000000030001f40000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001") + ); + } + + #[test] + fn wrap_ether_input_for_exact_input_single_hop() { + assert_eq!( + encode_route_to_path(&ROUTE_WETH_0, false), + hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000001") + ); + } + + #[test] + fn wrap_ether_input_for_exact_output_single_hop() { + assert_eq!( + encode_route_to_path(&ROUTE_WETH_0, true), + hex!("0000000000000000000000000000000000000001000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + ); + } + + #[test] + fn wrap_ether_input_for_exact_input_multihop() { + assert_eq!( + encode_route_to_path(&ROUTE_WETH_0_1, false), + hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002") + ); + } + + #[test] + fn wrap_ether_input_for_exact_output_multihop() { + assert_eq!( + encode_route_to_path(&ROUTE_WETH_0_1, true), + hex!("0000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + ); + } + + #[test] + fn wrap_ether_output_for_exact_input_single_hop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_WETH, false), + hex!("0000000000000000000000000000000000000001000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + ); + } + + #[test] + fn wrap_ether_output_for_exact_output_single_hop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_WETH, true), + hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000001") + ); + } + + #[test] + fn wrap_ether_output_for_exact_input_multihop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_1_WETH, false), + hex!("0000000000000000000000000000000000000001000bb80000000000000000000000000000000000000002000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + ); + } + + #[test] + fn wrap_ether_output_for_exact_output_multihop() { + assert_eq!( + encode_route_to_path(&ROUTE_0_1_WETH, true), + hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb80000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001") + ); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f3d1467..52588a0 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ mod bit_math; mod compute_pool_address; +mod encode_route_to_path; mod encode_sqrt_ratio_x96; mod full_math; mod get_fee_growth_inside; @@ -16,6 +17,7 @@ mod types; pub use bit_math::*; pub use compute_pool_address::compute_pool_address; +pub use encode_route_to_path::encode_route_to_path; pub use encode_sqrt_ratio_x96::encode_sqrt_ratio_x96; pub use full_math::*; pub use get_fee_growth_inside::*;