From 406a691ae2c1017f3ed158ec077bd1015bd69969 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:31:28 -0800 Subject: [PATCH 1/5] Enable optional inclusion of `extensions` module The `extensions` module has been made optional so that it can be included or excluded during the build using feature flags. This is reflected in the GitHub Actions workflow file where the `--all-features` flag is now used during build and testing. The version of the Uniswap V3 SDK is also upgraded to 0.9.0. --- .github/workflows/rust.yml | 4 +-- Cargo.lock | 59 +++++++++++++++++++++++++------------- Cargo.toml | 12 ++++++-- src/lib.rs | 9 ++++-- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0f93212..b2f9bbd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -31,7 +31,7 @@ jobs: ${{ runner.os }}-cargo-registry- - name: Build - run: cargo build + run: cargo build --all-features - name: Run tests - run: cargo test + run: cargo test --all-features diff --git a/Cargo.lock b/Cargo.lock index 5a5ba61..ad11aaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2029,9 +2029,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libm" @@ -2175,20 +2175,20 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 3.0.0", "proc-macro2", "quote", "syn 2.0.48", @@ -2266,7 +2266,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 1.0.109", @@ -2567,12 +2567,20 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2685dd208a3771337d8d386a89840f0f43cd68be8dae90a5f8c2384effc9cd" +dependencies = [ + "toml_edit 0.21.0", ] [[package]] @@ -3699,21 +3707,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.21.0", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -3731,9 +3739,20 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap", "serde", @@ -3891,7 +3910,7 @@ dependencies = [ [[package]] name = "uniswap-v3-sdk" -version = "0.8.2" +version = "0.9.0" dependencies = [ "alloy-primitives", "alloy-sol-types", diff --git a/Cargo.toml b/Cargo.toml index 3efb907..a801be0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-v3-sdk" -version = "0.8.2" +version = "0.9.0" edition = "2021" authors = ["Shuhui Luo "] description = "Uniswap V3 SDK for Rust" @@ -10,12 +10,15 @@ repository = "https://github.com/shuhuiluo/uniswap-v3-sdk-rs" keywords = ["uniswap-v3", "ethereum", "rust", "sdk"] exclude = [".github", ".gitignore", "rustfmt.toml"] +[package.metadata.docs.rs] +all-features = true + [dependencies] alloy-primitives = "0.5.4" alloy-sol-types = "0.5.4" anyhow = "1.0" -aperture-lens = "0.4.0" -ethers = "2.0" +aperture-lens = { version = "0.4.0", optional = true } +ethers = { version = "2.0", optional = true } num-bigint = "0.4.4" num-integer = "0.1.45" num-traits = "0.2.17" @@ -25,6 +28,9 @@ thiserror = "1.0.53" uniswap-sdk-core = "0.7.0" uniswap_v3_math = "0.4.1" +[features] +extensions = ["aperture-lens", "ethers"] + [dev-dependencies] criterion = "0.5.1" tokio = { version = "1.35", features = ["full"] } diff --git a/src/lib.rs b/src/lib.rs index 17d6ddd..6503a0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,14 @@ pub mod constants; pub mod entities; -pub mod extensions; pub mod utils; +#[cfg(feature = "extensions")] +pub mod extensions; + pub mod prelude { - pub use crate::{constants::*, entities::*, extensions::*, utils::*}; + pub use crate::{constants::*, entities::*, utils::*}; + + #[cfg(feature = "extensions")] + pub use crate::extensions::*; } From d87f987b74b83af7743fa16918df0313975e6c5f Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:00:50 -0800 Subject: [PATCH 2/5] Add `TickDataProvider` to `Pool` and refactor `EphemeralTickDataProvider` Integrate `TickDataProvider` into the `Pool` struct and encapsulated the fetching process within `EphemeralTickDataProvider`. The `TickListDataProvider` now includes `Default` trait and `EphemeralTickDataProvider` extracts minimum `tick_spacing` and can be converted into `TickListDataProvider`. `NoTickDataError` and `NoTickDataProvider` now address `Tick` data. --- src/entities/pool.rs | 59 +++++++++++-- src/entities/position.rs | 88 ++++++++++++------- src/entities/tick_data_provider.rs | 7 +- src/entities/tick_list_data_provider.rs | 9 +- .../ephemeral_tick_data_provider.rs | 85 ++++++++++-------- 5 files changed, 168 insertions(+), 80 deletions(-) diff --git a/src/entities/pool.rs b/src/entities/pool.rs index 80bb12e..f14ef48 100644 --- a/src/entities/pool.rs +++ b/src/entities/pool.rs @@ -2,6 +2,7 @@ use crate::prelude::*; use alloy_primitives::{Address, B256, U256}; use num_bigint::BigUint; use once_cell::sync::Lazy; +use std::sync::Arc; use uniswap_sdk_core::prelude::*; static _Q192: Lazy = Lazy::new(|| u256_to_big_uint(Q192)); @@ -15,6 +16,7 @@ pub struct Pool { pub sqrt_ratio_x96: U256, pub liquidity: u128, pub tick_current: i32, + pub tick_data_provider: Arc>, _token0_price: Option>, _token1_price: Option>, } @@ -47,12 +49,14 @@ impl Pool { /// * `sqrt_ratio_x96`: The sqrt of the current ratio of amounts of token1 to token0 /// * `liquidity`: The current value of in range liquidity /// * `tick_current`: The current tick of the pool + /// * `tick_data_provider`: A tick data provider that can return tick data pub fn new( token_a: Token, token_b: Token, fee: FeeAmount, sqrt_ratio_x96: U256, liquidity: u128, + tick_data_provider: Option>>, ) -> Self { let (token0, token1) = if token_a.sorts_before(&token_b) { (token_a, token_b) @@ -66,6 +70,7 @@ impl Pool { sqrt_ratio_x96, liquidity, tick_current: get_tick_at_sqrt_ratio(sqrt_ratio_x96).unwrap(), + tick_data_provider: tick_data_provider.unwrap_or(Arc::new(NoTickDataProvider)), _token0_price: None, _token1_price: None, } @@ -194,36 +199,78 @@ mod tests { #[should_panic(expected = "CHAIN_IDS")] fn test_constructor_cannot_be_used_for_tokens_on_different_chains() { let weth9 = WETH9::default().get(3).unwrap().clone(); - Pool::new(USDC.clone(), weth9.clone(), FeeAmount::MEDIUM, ONE_ETHER, 0); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::MEDIUM, + ONE_ETHER, + 0, + None, + ); } #[test] #[should_panic(expected = "ADDRESSES")] fn test_constructor_cannot_be_given_two_of_the_same_token() { - Pool::new(USDC.clone(), USDC.clone(), FeeAmount::MEDIUM, ONE_ETHER, 0); + Pool::new( + USDC.clone(), + USDC.clone(), + FeeAmount::MEDIUM, + ONE_ETHER, + 0, + None, + ); } #[test] fn test_constructor_works_with_valid_arguments_for_empty_pool_medium_fee() { let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new(USDC.clone(), weth9.clone(), FeeAmount::MEDIUM, ONE_ETHER, 0); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::MEDIUM, + ONE_ETHER, + 0, + None, + ); } #[test] fn test_constructor_works_with_valid_arguments_for_empty_pool_low_fee() { let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new(USDC.clone(), weth9.clone(), FeeAmount::LOW, ONE_ETHER, 0); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::LOW, + ONE_ETHER, + 0, + None, + ); } #[test] fn test_constructor_works_with_valid_arguments_for_empty_pool_lowest_fee() { let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new(USDC.clone(), weth9.clone(), FeeAmount::LOWEST, ONE_ETHER, 0); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::LOWEST, + ONE_ETHER, + 0, + None, + ); } #[test] fn test_constructor_works_with_valid_arguments_for_empty_pool_high_fee() { let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new(USDC.clone(), weth9.clone(), FeeAmount::HIGH, ONE_ETHER, 0); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::HIGH, + ONE_ETHER, + 0, + None, + ); } } diff --git a/src/entities/position.rs b/src/entities/position.rs index a8d958d..86bee92 100644 --- a/src/entities/position.rs +++ b/src/entities/position.rs @@ -212,6 +212,7 @@ impl Position { self.pool.fee, sqrt_ratio_x96_lower, 0, // liquidity doesn't matter + None, ); let pool_upper = Pool::new( self.pool.token0.clone(), @@ -219,6 +220,7 @@ impl Position { self.pool.fee, sqrt_ratio_x96_upper, 0, // liquidity doesn't matter + None, ); // Because the router is imprecise, we need to calculate the position that will be created (assuming no slippage) @@ -276,6 +278,7 @@ impl Position { self.pool.fee, sqrt_ratio_x96_lower, 0, // liquidity doesn't matter + None, ); let pool_upper = Pool::new( self.pool.token0.clone(), @@ -283,6 +286,7 @@ impl Position { self.pool.fee, sqrt_ratio_x96_upper, 0, // liquidity doesn't matter + None, ); // we want the smaller amounts... @@ -460,26 +464,28 @@ mod tests { static POOL_TICK_CURRENT: Lazy = Lazy::new(|| get_tick_at_sqrt_ratio(*POOL_SQRT_RATIO_START).unwrap()); const TICK_SPACING: i32 = FeeAmount::LOW.tick_spacing(); - static DAI_USDC_POOL: Lazy = Lazy::new(|| { + + fn dai_usdc_pool() -> Pool { Pool::new( DAI.clone(), USDC.clone(), FeeAmount::LOW, *POOL_SQRT_RATIO_START, 0, + None, ) - }); + } #[test] fn can_be_constructed_around_0_tick() { - let position = Position::new(DAI_USDC_POOL.clone(), 1, -10, 10); + let position = Position::new(dai_usdc_pool(), 1, -10, 10); assert_eq!(position.liquidity, 1); } #[test] fn can_use_min_and_max_ticks() { let position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 1, nearest_usable_tick(MIN_TICK, TICK_SPACING), nearest_usable_tick(MAX_TICK, TICK_SPACING), @@ -490,26 +496,26 @@ mod tests { #[test] #[should_panic(expected = "TICK_ORDER")] fn tick_lower_must_be_less_than_tick_upper() { - Position::new(DAI_USDC_POOL.clone(), 1, 10, -10); + Position::new(dai_usdc_pool(), 1, 10, -10); } #[test] #[should_panic(expected = "TICK_ORDER")] fn tick_lower_cannot_equal_tick_upper() { - Position::new(DAI_USDC_POOL.clone(), 1, -10, -10); + Position::new(dai_usdc_pool(), 1, -10, -10); } #[test] #[should_panic(expected = "TICK_LOWER")] fn tick_lower_must_be_multiple_of_tick_spacing() { - Position::new(DAI_USDC_POOL.clone(), 1, -5, 10); + Position::new(dai_usdc_pool(), 1, -5, 10); } #[test] #[should_panic(expected = "TICK_LOWER")] fn tick_lower_must_be_greater_than_min_tick() { Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 1, nearest_usable_tick(MIN_TICK, TICK_SPACING) - TICK_SPACING, 10, @@ -519,14 +525,14 @@ mod tests { #[test] #[should_panic(expected = "TICK_UPPER")] fn tick_upper_must_be_multiple_of_tick_spacing() { - Position::new(DAI_USDC_POOL.clone(), 1, -10, 15); + Position::new(dai_usdc_pool(), 1, -10, 15); } #[test] #[should_panic(expected = "TICK_UPPER")] fn tick_upper_must_be_less_than_max_tick() { Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 1, -10, nearest_usable_tick(MAX_TICK, TICK_SPACING) + TICK_SPACING, @@ -536,7 +542,7 @@ mod tests { #[test] fn amount0_is_correct_for_price_above() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e12 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -550,7 +556,7 @@ mod tests { #[test] fn amount0_is_correct_for_price_below() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -561,7 +567,7 @@ mod tests { #[test] fn amount0_is_correct_for_in_range_position() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -575,7 +581,7 @@ mod tests { #[test] fn amount1_is_correct_for_price_above() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -586,7 +592,7 @@ mod tests { #[test] fn amount1_is_correct_for_price_below() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -597,7 +603,7 @@ mod tests { #[test] fn amount1_is_correct_for_in_range_position() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -608,7 +614,7 @@ mod tests { #[test] fn mint_amounts_with_slippage_is_correct_for_positions_below() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -623,7 +629,7 @@ mod tests { #[test] fn mint_amounts_with_slippage_is_correct_for_positions_above() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -638,7 +644,7 @@ mod tests { #[test] fn mint_amounts_with_slippage_is_correct_for_positions_within() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -653,7 +659,7 @@ mod tests { #[test] fn mint_amounts_with_slippage_is_correct_for_positions_below_05_percent_slippage() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -668,7 +674,7 @@ mod tests { #[test] fn mint_amounts_with_slippage_is_correct_for_positions_above_05_percent_slippage() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -683,7 +689,7 @@ mod tests { #[test] fn mint_amounts_with_slippage_is_correct_for_positions_within_05_percent_slippage() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -698,7 +704,14 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_pool_at_min_price() { let mut position = Position::new( - Pool::new(DAI.clone(), USDC.clone(), FeeAmount::LOW, MIN_SQRT_RATIO, 0), + Pool::new( + DAI.clone(), + USDC.clone(), + FeeAmount::LOW, + MIN_SQRT_RATIO, + 0, + None, + ), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -718,6 +731,7 @@ mod tests { FeeAmount::LOW, MAX_SQRT_RATIO - U256::from_limbs([1, 0, 0, 0]), 0, + None, ), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, @@ -732,7 +746,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_below() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -746,7 +760,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_above() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -760,7 +774,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_within() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -774,7 +788,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_below_05_percent_slippage() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -788,7 +802,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_above_05_percent_slippage() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -802,7 +816,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_within_05_percent_slippage() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -816,7 +830,14 @@ mod tests { #[test] fn mint_amounts_is_correct_for_pool_at_min_price() { let mut position = Position::new( - Pool::new(DAI.clone(), USDC.clone(), FeeAmount::LOW, MIN_SQRT_RATIO, 0), + Pool::new( + DAI.clone(), + USDC.clone(), + FeeAmount::LOW, + MIN_SQRT_RATIO, + 0, + None, + ), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -837,6 +858,7 @@ mod tests { FeeAmount::LOW, MAX_SQRT_RATIO - U256::from_limbs([1, 0, 0, 0]), 0, + None, ), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, @@ -852,7 +874,7 @@ mod tests { #[test] fn mint_amounts_is_correct_for_positions_above() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, @@ -865,7 +887,7 @@ mod tests { #[test] fn mint_amounts_is_correct_for_positions_below() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING, @@ -878,7 +900,7 @@ mod tests { #[test] fn mint_amounts_is_correct_for_positions_within() { let mut position = Position::new( - DAI_USDC_POOL.clone(), + dai_usdc_pool(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING * 2, diff --git a/src/entities/tick_data_provider.rs b/src/entities/tick_data_provider.rs index cbf6413..3008ad8 100644 --- a/src/entities/tick_data_provider.rs +++ b/src/entities/tick_data_provider.rs @@ -1,3 +1,4 @@ +use crate::entities::Tick; use anyhow::Result; use thiserror::Error; @@ -33,7 +34,7 @@ pub trait TickDataProvider { ) -> Result<(i32, bool)>; } -#[derive(Error, Debug)] +#[derive(Clone, Debug, Error)] #[error("No tick data provider was given")] pub struct NoTickDataError; @@ -42,9 +43,9 @@ pub struct NoTickDataError; pub struct NoTickDataProvider; impl TickDataProvider for NoTickDataProvider { - type Tick = (); + type Tick = Tick; - fn get_tick(&self, _: i32) -> Result<&()> { + fn get_tick(&self, _: i32) -> Result<&Tick> { Err(NoTickDataError.into()) } diff --git a/src/entities/tick_list_data_provider.rs b/src/entities/tick_list_data_provider.rs index efbd45f..0dc5664 100644 --- a/src/entities/tick_list_data_provider.rs +++ b/src/entities/tick_list_data_provider.rs @@ -5,6 +5,7 @@ use crate::{ use anyhow::Result; /// A data provider for ticks that is backed by an in-memory array of ticks. +#[derive(Clone)] pub struct TickListDataProvider(Vec); impl TickListDataProvider { @@ -14,6 +15,12 @@ impl TickListDataProvider { } } +impl Default for TickListDataProvider { + fn default() -> Self { + Self(vec![]) + } +} + impl TickDataProvider for TickListDataProvider { type Tick = Tick; @@ -43,7 +50,7 @@ mod tests { #[test] fn can_take_an_empty_list_of_ticks() { - TickListDataProvider::new(vec![], 1); + TickListDataProvider::default(); } #[test] diff --git a/src/extensions/ephemeral_tick_data_provider.rs b/src/extensions/ephemeral_tick_data_provider.rs index 445b92f..f823587 100644 --- a/src/extensions/ephemeral_tick_data_provider.rs +++ b/src/extensions/ephemeral_tick_data_provider.rs @@ -7,51 +7,56 @@ use std::sync::Arc; /// A data provider for ticks that fetches ticks using an ephemeral contract in a single `eth_call`. #[derive(Clone)] -pub struct EphemeralTickDataProvider { +pub struct EphemeralTickDataProvider { pub pool: Address, - client: Arc, pub tick_lower: i32, pub tick_upper: i32, pub block_id: Option, pub ticks: Vec, + // the minimum distance between two ticks in the list + pub tick_spacing: i32, } -impl EphemeralTickDataProvider { - pub fn new( +impl EphemeralTickDataProvider { + pub async fn new( pool: Address, client: Arc, tick_lower: Option, tick_upper: Option, block_id: Option, - ) -> Self { - Self { - pool, - tick_lower: tick_lower.unwrap_or(MIN_TICK), - tick_upper: tick_upper.unwrap_or(MAX_TICK), - client, - block_id, - ticks: Vec::new(), - } - } - - pub async fn fetch(&mut self) -> Result<(), ContractError> { + ) -> Result> { + let tick_lower = tick_lower.unwrap_or(MIN_TICK); + let tick_upper = tick_upper.unwrap_or(MAX_TICK); let ticks = get_populated_ticks_in_range( - self.pool.into_array().into(), - self.tick_lower, - self.tick_upper, - self.client.clone(), - self.block_id, + pool.into_array().into(), + tick_lower, + tick_upper, + client.clone(), + block_id, ) .await?; - self.ticks = ticks + let ticks: Vec<_> = ticks .into_iter() .map(|tick| Tick::new(tick.tick, tick.liquidity_gross, tick.liquidity_net)) .collect(); - Ok(()) + let tick_indices: Vec<_> = ticks.iter().map(|tick| tick.index).collect(); + let tick_spacing = tick_indices + .windows(2) + .map(|window| window[1] - window[0]) + .min() + .unwrap(); + Ok(Self { + pool, + tick_lower, + tick_upper, + block_id, + ticks, + tick_spacing, + }) } } -impl TickDataProvider for EphemeralTickDataProvider { +impl TickDataProvider for EphemeralTickDataProvider { type Tick = Tick; fn get_tick(&self, tick: i32) -> Result<&Tick> { @@ -70,29 +75,31 @@ impl TickDataProvider for EphemeralTickDataProvider { } } +impl From for TickListDataProvider { + fn from(provider: EphemeralTickDataProvider) -> Self { + assert!(!provider.ticks.is_empty()); + Self::new(provider.ticks, provider.tick_spacing) + } +} + #[cfg(test)] mod tests { use super::*; use alloy_primitives::address; - use ethers::prelude::{Http, Provider, MAINNET}; - use once_cell::sync::Lazy; + use ethers::prelude::MAINNET; - static PROVIDER: Lazy>> = Lazy::new(|| { - let provider = Arc::new(MAINNET.provider()); - EphemeralTickDataProvider::new( + const TICK_SPACING: i32 = 10; + + #[tokio::test] + async fn test_ephemeral_tick_data_provider() -> Result<()> { + let provider = EphemeralTickDataProvider::new( address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), - provider, + Arc::new(MAINNET.provider()), None, None, Some(BlockId::from(17000000)), ) - }); - const TICK_SPACING: i32 = 10; - - #[tokio::test] - async fn test_ephemeral_tick_data_provider() -> Result<()> { - let mut provider = PROVIDER.clone(); - provider.fetch().await?; + .await?; assert!(!provider.ticks.is_empty()); provider.ticks.validate_list(TICK_SPACING); let tick = provider.get_tick(-92110)?; @@ -109,6 +116,10 @@ mod tests { provider.next_initialized_tick_within_one_word(0, false, TICK_SPACING)?; assert!(success); assert_eq!(tick, 100); + let provider: TickListDataProvider = provider.into(); + let tick = provider.get_tick(-92110)?; + assert_eq!(tick.liquidity_gross, 398290794261); + assert_eq!(tick.liquidity_net, 398290794261); Ok(()) } } From 08d71eb560ba0109cb9e2d52c533f0ffcd6f275c Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Tue, 9 Jan 2024 05:45:56 -0800 Subject: [PATCH 3/5] Implement pool swap functions and add relevant tests The commit introduces logic for `get_output_amount` and `get_input_amount` in the `Pool` struct for handling token swaps. It also includes a helper function, `_swap`, used to facilitate these transactions. Test cases have been added to ensure the correct functionality and reliability of these new features. --- src/entities/pool.rs | 544 +++++++++++++++++++++++++++++++++++++++---- src/utils/mod.rs | 21 +- 2 files changed, 512 insertions(+), 53 deletions(-) diff --git a/src/entities/pool.rs b/src/entities/pool.rs index f14ef48..240a9d2 100644 --- a/src/entities/pool.rs +++ b/src/entities/pool.rs @@ -1,7 +1,8 @@ use crate::prelude::*; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{Address, B256, I256, U256}; use num_bigint::BigUint; use once_cell::sync::Lazy; +use std::ops::Neg; use std::sync::Arc; use uniswap_sdk_core::prelude::*; @@ -21,6 +22,24 @@ pub struct Pool { _token1_price: Option>, } +struct SwapState { + amount_specified_remaining: I256, + amount_calculated: I256, + sqrt_price_x96: U256, + tick: i32, + liquidity: u128, +} + +struct StepComputations { + sqrt_price_start_x96: U256, + tick_next: i32, + initialized: bool, + sqrt_price_next_x96: U256, + amount_in: U256, + amount_out: U256, + fee_amount: U256, +} + impl Pool { /// Compute the pool address pub fn get_address( @@ -143,29 +162,216 @@ impl Pool { } } - pub async fn get_output_amount( + /// Given an input amount of a token, return the computed output amount, and a pool with state updated after the trade + /// + /// # Arguments + /// + /// * `input_amount`: The input amount for which to quote the output amount + /// * `sqrt_price_limit_x96`: The Q64.96 sqrt price limit + /// + /// returns: The output amount and the pool with updated state + /// + pub fn get_output_amount( &self, - _input_amount: CurrencyAmount, - _sqrt_price_limit_x96: Option, + input_amount: CurrencyAmount, + sqrt_price_limit_x96: Option, ) -> (CurrencyAmount, Self) { - todo!("get_output_amount") + assert!(self.involves_token(&input_amount.meta.currency), "TOKEN"); + + let zero_for_one = input_amount.meta.currency.equals(&self.token0); + + let (output_amount, sqrt_ratio_x96, liquidity, _) = self._swap( + zero_for_one, + big_int_to_i256(input_amount.quotient()), + sqrt_price_limit_x96, + ); + let output_token = if zero_for_one { + self.token1.clone() + } else { + self.token0.clone() + }; + ( + CurrencyAmount::from_raw_amount(output_token, i256_to_big_int(output_amount.neg())), + Pool::new( + self.token0.clone(), + self.token1.clone(), + self.fee, + sqrt_ratio_x96, + liquidity, + Some(self.tick_data_provider.clone()), + ), + ) } - pub async fn get_input_amount( + /// Given a desired output amount of a token, return the computed input amount and a pool with state updated after the trade + /// + /// # Arguments + /// + /// * `output_amount`: the output amount for which to quote the input amount + /// * `sqrt_price_limit_x96`: The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this value + /// after the swap. If one for zero, the price cannot be greater than this value after the swap + /// + /// returns: The input amount and the pool with updated state + /// + pub fn get_input_amount( &self, - _output_amount: CurrencyAmount, - _sqrt_price_limit_x96: Option, + output_amount: CurrencyAmount, + sqrt_price_limit_x96: Option, ) -> (CurrencyAmount, Self) { - todo!("get_input_amount") + assert!(self.involves_token(&output_amount.meta.currency), "TOKEN"); + + let zero_for_one = output_amount.meta.currency.equals(&self.token1); + + let (input_amount, sqrt_ratio_x96, liquidity, _) = self._swap( + zero_for_one, + big_int_to_i256(output_amount.quotient()).neg(), + sqrt_price_limit_x96, + ); + let input_token = if zero_for_one { + self.token0.clone() + } else { + self.token1.clone() + }; + ( + CurrencyAmount::from_raw_amount(input_token, i256_to_big_int(input_amount)), + Pool::new( + self.token0.clone(), + self.token1.clone(), + self.fee, + sqrt_ratio_x96, + liquidity, + Some(self.tick_data_provider.clone()), + ), + ) } - async fn _swap( + fn _swap( &self, - _zero_for_one: bool, - _amount_specified: U256, - _sqrt_price_limit_x96: Option, - ) -> (U256, U256, u128, i32) { - todo!("swap") + zero_for_one: bool, + amount_specified: I256, + sqrt_price_limit_x96: Option, + ) -> (I256, U256, u128, i32) { + const ONE: U256 = U256::from_limbs([1, 0, 0, 0]); + let sqrt_price_limit_x96 = sqrt_price_limit_x96.unwrap_or_else(|| { + if zero_for_one { + MIN_SQRT_RATIO + ONE + } else { + MAX_SQRT_RATIO - ONE + } + }); + + if zero_for_one { + assert!(sqrt_price_limit_x96 > MIN_SQRT_RATIO, "RATIO_MIN"); + assert!(sqrt_price_limit_x96 < self.sqrt_ratio_x96, "RATIO_CURRENT"); + } else { + assert!(sqrt_price_limit_x96 < MAX_SQRT_RATIO, "RATIO_MAX"); + assert!(sqrt_price_limit_x96 > self.sqrt_ratio_x96, "RATIO_CURRENT"); + } + + let exact_input = amount_specified >= I256::ZERO; + + // keep track of swap state + let mut state = SwapState { + amount_specified_remaining: amount_specified, + amount_calculated: I256::ZERO, + sqrt_price_x96: self.sqrt_ratio_x96, + tick: self.tick_current, + liquidity: self.liquidity, + }; + + // start swap while loop + while !state.amount_specified_remaining.is_zero() + && state.sqrt_price_x96 != sqrt_price_limit_x96 + { + let mut step = StepComputations { + sqrt_price_start_x96: state.sqrt_price_x96, + tick_next: 0, + initialized: false, + sqrt_price_next_x96: U256::ZERO, + amount_in: U256::ZERO, + amount_out: U256::ZERO, + fee_amount: U256::ZERO, + }; + + step.sqrt_price_start_x96 = state.sqrt_price_x96; + // because each iteration of the while loop rounds, we can't optimize this code (relative to the smart contract) + // by simply traversing to the next available tick, we instead need to exactly replicate + (step.tick_next, step.initialized) = self + .tick_data_provider + .next_initialized_tick_within_one_word( + state.tick, + zero_for_one, + self.tick_spacing(), + ) + .unwrap(); + + if step.tick_next < MIN_TICK { + step.tick_next = MIN_TICK; + } else if step.tick_next > MAX_TICK { + step.tick_next = MAX_TICK; + } + + step.sqrt_price_next_x96 = get_sqrt_ratio_at_tick(step.tick_next).unwrap(); + ( + state.sqrt_price_x96, + step.amount_in, + step.amount_out, + step.fee_amount, + ) = compute_swap_step( + state.sqrt_price_x96, + if zero_for_one { + step.sqrt_price_next_x96.max(sqrt_price_limit_x96) + } else { + step.sqrt_price_next_x96.min(sqrt_price_limit_x96) + }, + state.liquidity, + state.amount_specified_remaining, + self.fee as u32, + ) + .unwrap(); + + if exact_input { + state.amount_specified_remaining = I256::from_raw( + state.amount_specified_remaining.into_raw() - step.amount_in - step.fee_amount, + ); + state.amount_calculated = + I256::from_raw(state.amount_calculated.into_raw() - step.amount_out); + } else { + state.amount_specified_remaining = + I256::from_raw(state.amount_specified_remaining.into_raw() + step.amount_out); + state.amount_calculated = I256::from_raw( + state.amount_calculated.into_raw() + step.amount_in + step.fee_amount, + ); + } + + if state.sqrt_price_x96 == step.sqrt_price_next_x96 { + // if the tick is initialized, run the tick transition + if step.initialized { + let mut liquidity_net = self + .tick_data_provider + .get_tick(step.tick_next) + .unwrap() + .liquidity_net; + // if we're moving leftward, we interpret liquidityNet as the opposite sign + // safe because liquidityNet cannot be type(int128).min + if zero_for_one { + liquidity_net = liquidity_net.neg(); + } + state.liquidity = add_delta(state.liquidity, liquidity_net).unwrap(); + } + state.tick = step.tick_next - zero_for_one as i32; + } else { + // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved + state.tick = get_tick_at_sqrt_ratio(state.sqrt_price_x96).unwrap(); + } + } + + ( + state.amount_calculated, + state.sqrt_price_x96, + state.liquidity, + state.tick, + ) } } @@ -185,7 +391,7 @@ mod tests { "USD Coin" ) }); - static _DAI: Lazy = Lazy::new(|| { + static DAI: Lazy = Lazy::new(|| { token!( 1, "0x6B175474E89094C44Da98b954EedeAC495271d0F", @@ -195,82 +401,318 @@ mod tests { ) }); + mod constructor { + use super::*; + + #[test] + #[should_panic(expected = "CHAIN_IDS")] + fn cannot_be_used_for_tokens_on_different_chains() { + let weth9 = WETH9::default().get(3).unwrap().clone(); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::MEDIUM, + ONE_ETHER, + 0, + None, + ); + } + + #[test] + #[should_panic(expected = "ADDRESSES")] + fn cannot_be_given_two_of_the_same_token() { + Pool::new( + USDC.clone(), + USDC.clone(), + FeeAmount::MEDIUM, + ONE_ETHER, + 0, + None, + ); + } + + #[test] + fn works_with_valid_arguments_for_empty_pool_medium_fee() { + let weth9 = WETH9::default().get(1).unwrap().clone(); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::MEDIUM, + ONE_ETHER, + 0, + None, + ); + } + + #[test] + fn works_with_valid_arguments_for_empty_pool_low_fee() { + let weth9 = WETH9::default().get(1).unwrap().clone(); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::LOW, + ONE_ETHER, + 0, + None, + ); + } + + #[test] + fn works_with_valid_arguments_for_empty_pool_lowest_fee() { + let weth9 = WETH9::default().get(1).unwrap().clone(); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::LOWEST, + ONE_ETHER, + 0, + None, + ); + } + + #[test] + fn works_with_valid_arguments_for_empty_pool_high_fee() { + let weth9 = WETH9::default().get(1).unwrap().clone(); + Pool::new( + USDC.clone(), + weth9.clone(), + FeeAmount::HIGH, + ONE_ETHER, + 0, + None, + ); + } + } + #[test] - #[should_panic(expected = "CHAIN_IDS")] - fn test_constructor_cannot_be_used_for_tokens_on_different_chains() { - let weth9 = WETH9::default().get(3).unwrap().clone(); - Pool::new( + fn get_address_matches_an_example() { + let result = Pool::get_address(&USDC, &DAI, FeeAmount::LOW, None, None); + assert_eq!(result, address!("6c6Bc977E13Df9b0de53b251522280BB72383700")); + } + + #[test] + fn token0_always_is_the_token_that_sorts_before() { + let pool = Pool::new( + USDC.clone(), + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + 0, + None, + ); + assert!(pool.token0.equals(&DAI.clone())); + let pool = Pool::new( + DAI.clone(), + USDC.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + 0, + None, + ); + assert!(pool.token0.equals(&DAI.clone())); + } + + #[test] + fn token1_always_is_the_token_that_sorts_after() { + let pool = Pool::new( + USDC.clone(), + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + 0, + None, + ); + assert!(pool.token1.equals(&USDC.clone())); + let pool = Pool::new( + DAI.clone(), + USDC.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + 0, + None, + ); + assert!(pool.token1.equals(&USDC.clone())); + } + + #[test] + fn token0_price_returns_price_of_token0_in_terms_of_token1() { + let mut pool = Pool::new( + USDC.clone(), + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(101e6 as u128, 100e18 as u128), + 0, + None, + ); + assert_eq!( + pool.token0_price().to_significant(5, Rounding::RoundHalfUp), + "1.01" + ); + let mut pool = Pool::new( + DAI.clone(), USDC.clone(), - weth9.clone(), - FeeAmount::MEDIUM, - ONE_ETHER, + FeeAmount::LOW, + encode_sqrt_ratio_x96(101e6 as u128, 100e18 as u128), 0, None, ); + assert_eq!( + pool.token0_price().to_significant(5, Rounding::RoundHalfUp), + "1.01" + ); } #[test] - #[should_panic(expected = "ADDRESSES")] - fn test_constructor_cannot_be_given_two_of_the_same_token() { - Pool::new( + fn token1_price_returns_price_of_token1_in_terms_of_token0() { + let mut pool = Pool::new( USDC.clone(), + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(101e6 as u128, 100e18 as u128), + 0, + None, + ); + assert_eq!( + pool.token1_price().to_significant(5, Rounding::RoundHalfUp), + "0.9901" + ); + let mut pool = Pool::new( + DAI.clone(), USDC.clone(), - FeeAmount::MEDIUM, - ONE_ETHER, + FeeAmount::LOW, + encode_sqrt_ratio_x96(101e6 as u128, 100e18 as u128), 0, None, ); + assert_eq!( + pool.token1_price().to_significant(5, Rounding::RoundHalfUp), + "0.9901" + ); } #[test] - fn test_constructor_works_with_valid_arguments_for_empty_pool_medium_fee() { - let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new( + fn price_of_returns_price_of_token_in_terms_of_other_token() { + let mut pool = Pool::new( USDC.clone(), - weth9.clone(), - FeeAmount::MEDIUM, - ONE_ETHER, + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), 0, None, ); + assert!(pool.price_of(&DAI.clone()).equal_to(&pool.token0_price())); + assert!(pool.price_of(&USDC.clone()).equal_to(&pool.token1_price())); } #[test] - fn test_constructor_works_with_valid_arguments_for_empty_pool_low_fee() { - let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new( + #[should_panic(expected = "TOKEN")] + fn price_of_throws_if_invalid_token() { + let mut pool = Pool::new( USDC.clone(), - weth9.clone(), + DAI.clone(), FeeAmount::LOW, - ONE_ETHER, + encode_sqrt_ratio_x96(1, 1), 0, None, ); + pool.price_of(&WETH9::default().get(1).unwrap().clone()); } #[test] - fn test_constructor_works_with_valid_arguments_for_empty_pool_lowest_fee() { - let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new( + fn chain_id_returns_token0_chain_id() { + let pool = Pool::new( + USDC.clone(), + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + 0, + None, + ); + assert_eq!(pool.chain_id(), 1); + let pool = Pool::new( + DAI.clone(), USDC.clone(), - weth9.clone(), - FeeAmount::LOWEST, - ONE_ETHER, + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), 0, None, ); + assert_eq!(pool.chain_id(), 1); } #[test] - fn test_constructor_works_with_valid_arguments_for_empty_pool_high_fee() { - let weth9 = WETH9::default().get(1).unwrap().clone(); - Pool::new( + fn involves_token() { + let pool = Pool::new( USDC.clone(), - weth9.clone(), - FeeAmount::HIGH, - ONE_ETHER, + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), 0, None, ); + assert!(pool.involves_token(&USDC.clone())); + assert!(pool.involves_token(&DAI.clone())); + assert!(!pool.involves_token(&WETH9::default().get(1).unwrap().clone())); + } + + mod swaps { + use super::*; + + fn pool() -> Pool { + Pool::new( + USDC.clone(), + DAI.clone(), + FeeAmount::LOW, + encode_sqrt_ratio_x96(1, 1), + ONE_ETHER.into_limbs()[0] as u128, + Some(Arc::new(TickListDataProvider::new( + vec![ + Tick::new( + nearest_usable_tick(MIN_TICK, FeeAmount::LOW.tick_spacing()), + ONE_ETHER.into_limbs()[0] as u128, + ONE_ETHER.into_limbs()[0] as i128, + ), + Tick::new( + nearest_usable_tick(MAX_TICK, FeeAmount::LOW.tick_spacing()), + ONE_ETHER.into_limbs()[0] as u128, + -(ONE_ETHER.into_limbs()[0] as i128), + ), + ], + FeeAmount::LOW.tick_spacing(), + ))), + ) + } + + #[test] + fn get_output_amount_usdc_to_dai() { + let (output_amount, _) = + pool().get_output_amount(CurrencyAmount::from_raw_amount(USDC.clone(), 100), None); + assert!(output_amount.meta.currency.equals(&DAI.clone())); + assert_eq!(output_amount.quotient(), 98.into()); + } + + #[test] + fn get_output_amount_dai_to_usdc() { + let (output_amount, _) = + pool().get_output_amount(CurrencyAmount::from_raw_amount(DAI.clone(), 100), None); + assert!(output_amount.meta.currency.equals(&USDC.clone())); + assert_eq!(output_amount.quotient(), 98.into()); + } + + #[test] + fn get_input_amount_usdc_to_dai() { + let (input_amount, _) = + pool().get_input_amount(CurrencyAmount::from_raw_amount(DAI.clone(), 98), None); + assert!(input_amount.meta.currency.equals(&USDC.clone())); + assert_eq!(input_amount.quotient(), 100.into()); + } + + #[test] + fn get_input_amount_dai_to_usdc() { + let (input_amount, _) = + pool().get_input_amount(CurrencyAmount::from_raw_amount(USDC.clone(), 98), None); + assert!(input_amount.meta.currency.equals(&DAI.clone())); + assert_eq!(input_amount.quotient(), 100.into()); + } } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index fd63243..66ed9f7 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -28,9 +28,10 @@ pub use swap_math::compute_swap_step; pub use tick_list::TickList; pub use tick_math::*; -use alloy_primitives::U256; +use alloy_primitives::{I256, U256}; use num_bigint::{BigInt, BigUint, Sign}; -use num_traits::ToBytes; +use num_traits::{Signed, ToBytes}; +use std::ops::Neg; pub const Q96: U256 = U256::from_limbs([0, 4294967296, 0, 0]); pub const Q128: U256 = U256::from_limbs([0, 0, 1, 0]); @@ -44,6 +45,14 @@ pub fn u256_to_big_int(x: U256) -> BigInt { BigInt::from_bytes_be(Sign::Plus, &x.to_be_bytes::<32>()) } +pub fn i256_to_big_int(x: I256) -> BigInt { + if x.is_positive() { + u256_to_big_int(x.into_raw()) + } else { + u256_to_big_int(x.neg().into_raw()).neg() + } +} + pub fn big_uint_to_u256(x: BigUint) -> U256 { U256::from_be_slice(&x.to_be_bytes()) } @@ -51,3 +60,11 @@ pub fn big_uint_to_u256(x: BigUint) -> U256 { pub fn big_int_to_u256(x: BigInt) -> U256 { U256::from_be_slice(&x.to_be_bytes()) } + +pub fn big_int_to_i256(x: BigInt) -> I256 { + if x.is_positive() { + I256::from_raw(big_int_to_u256(x)) + } else { + I256::from_raw(big_int_to_u256(x.neg())).neg() + } +} From afab0b7abddc1b39d16382dcb62fd41403f7ccb7 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Tue, 9 Jan 2024 05:51:18 -0800 Subject: [PATCH 4/5] make clippy happy --- src/entities/tick_list_data_provider.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/entities/tick_list_data_provider.rs b/src/entities/tick_list_data_provider.rs index 0dc5664..e160035 100644 --- a/src/entities/tick_list_data_provider.rs +++ b/src/entities/tick_list_data_provider.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::Result; /// A data provider for ticks that is backed by an in-memory array of ticks. -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct TickListDataProvider(Vec); impl TickListDataProvider { @@ -15,12 +15,6 @@ impl TickListDataProvider { } } -impl Default for TickListDataProvider { - fn default() -> Self { - Self(vec![]) - } -} - impl TickDataProvider for TickListDataProvider { type Tick = Tick; From d983e996e7533e0e7e18bbc078133e7a8c582b98 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Tue, 9 Jan 2024 05:58:06 -0800 Subject: [PATCH 5/5] cargo fmt --- src/entities/pool.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/entities/pool.rs b/src/entities/pool.rs index 240a9d2..11718ac 100644 --- a/src/entities/pool.rs +++ b/src/entities/pool.rs @@ -2,8 +2,7 @@ use crate::prelude::*; use alloy_primitives::{Address, B256, I256, U256}; use num_bigint::BigUint; use once_cell::sync::Lazy; -use std::ops::Neg; -use std::sync::Arc; +use std::{ops::Neg, sync::Arc}; use uniswap_sdk_core::prelude::*; static _Q192: Lazy = Lazy::new(|| u256_to_big_uint(Q192));