From 55a44fbaa21cd5ea0f54bbe4cff62e29c025a4a2 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Sat, 2 Mar 2024 11:43:01 -0500 Subject: [PATCH] chore(refactor): Refactor and improve documentation (#40) * Update package versions and reorganize imports The 'uniswap-v3-sdk' version has been updated to 0.25.0, along with other dependencies like 'base64' and 'ruint'. The code has also been revised to use 'alloy_primitives' for 'hex' and 'uint' imports, to bring some consistency to the modular imports. Changes have also been made on how 'pool' features are imported in 'entities/mod.rs'. * Add benchmarks to README A section for performance benchmarks has been added to the README file. It includes a detailed table showing the time taken and reference time for different functions, which can be expanded for viewing. * Refactor pool, route and position entities code Removed dependence on mutable self in methods for Pool, Route, and Position objects. Price-deriving methods in the Pool object were simplified to always calculate price instead of storing it. Also, the Debug and PartialEq trait implementations were relocated to follow Rust idioms. * Add features section to documentation A "Features" section has been added to the initial comments in the lib.rs file. This section outlines several important functionalities of the Rust Uniswap V3 SDK, such as its usage of alloy-rs types, the reimplementation of math libraries, the unit tests, benchmarks and extended capabilities for additional Uniswap V3 related functionalities. * Add doctest for `compute_pool_address` The function compute_pool_address now includes comprehensive usage examples directly in its documentation. This provides clear, practical guidance for developers on how to use the function. The related tests that were previously in a separate module have been removed as they are now redundant. --- Cargo.toml | 10 +-- README.md | 14 +++- src/entities/mod.rs | 2 +- src/entities/pool.rs | 106 ++++++++++++---------------- src/entities/position.rs | 63 ++++++++--------- src/entities/route.rs | 2 +- src/lib.rs | 19 +++++ src/nonfungible_position_manager.rs | 2 +- src/swap_router.rs | 3 +- src/tests.rs | 2 +- src/utils/compute_pool_address.rs | 65 +++++++++-------- 11 files changed, 151 insertions(+), 137 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6a3a628..4c4926e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-v3-sdk" -version = "0.24.2" +version = "0.25.0" edition = "2021" authors = ["Shuhui Luo "] description = "Uniswap V3 SDK for Rust" @@ -18,7 +18,7 @@ alloy-primitives = "0.6" alloy-sol-types = "0.6" anyhow = "1.0" aperture-lens = { version = "0.4", optional = true } -base64 = { version = "0.21.7", optional = true } +base64 = { version = "0.22", optional = true } bigdecimal = "0.4.2" ethers = { version = "2.0", optional = true } ethers-core = "2.0" @@ -27,11 +27,11 @@ num-integer = "0.1.45" num-traits = "0.2.17" once_cell = "1.19" regex = { version = "1.10", optional = true } -ruint = "1.11" +ruint = "1.12" serde_json = { version = "1.0", optional = true } thiserror = "1.0" -uniswap-sdk-core = "0.16.0" -uniswap_v3_math = "0.4.1" +uniswap-sdk-core = "0.16" +uniswap_v3_math = "0.4" [features] extensions = ["aperture-lens", "base64", "ethers", "regex", "serde_json"] diff --git a/README.md b/README.md index 75d2b5d..c097e91 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,24 @@ expect. The error handling is still a work in progress. an [ephemeral contract](https://github.com/Aperture-Finance/Aperture-Lens/blob/904101e4daed59e02fd4b758b98b0749e70b583b/contracts/EphemeralGetPopulatedTicksInRange.sol) in a single `eth_call` +
+ Expand to see the benchmarks + +| Function | Time | Reference | +|------------------------|-----------|-----------| +| most_significant_bit | 8.3693 µs | 39.691 µs | +| least_significant_bit | 5.0592 µs | 16.619 µs | +| get_sqrt_ratio_at_tick | 5.2105 µs | 71.137 µs | +| get_tick_at_sqrt_ratio | 34.331 µs | 191.08 µs | + +
+ ## Getting started Add the following to your `Cargo.toml` file: ```toml -uniswap-v3-sdk = { version = "0.23.0", features = ["extensions"] } +uniswap-v3-sdk = { version = "0.25.0", features = ["extensions"] } ``` ### Usage diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 0d21339..0a87f62 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -6,7 +6,7 @@ mod tick_data_provider; mod tick_list_data_provider; mod trade; -pub use pool::Pool; +pub use pool::{get_address, Pool}; pub use position::{MintAmounts, Position}; pub use route::Route; pub use tick::{Tick, TickTrait}; diff --git a/src/entities/pool.rs b/src/entities/pool.rs index 73712e5..44350d7 100644 --- a/src/entities/pool.rs +++ b/src/entities/pool.rs @@ -17,8 +17,30 @@ pub struct Pool

{ pub liquidity: u128, pub tick_current: i32, pub tick_data_provider: P, - _token0_price: Option>, - _token1_price: Option>, +} + +impl

fmt::Debug for Pool

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Pool") + .field("token0", &self.token0) + .field("token1", &self.token1) + .field("fee", &self.fee) + .field("sqrt_ratio_x96", &self.sqrt_ratio_x96) + .field("liquidity", &self.liquidity) + .field("tick_current", &self.tick_current) + .finish() + } +} + +impl

PartialEq for Pool

{ + fn eq(&self, other: &Self) -> bool { + self.token0 == other.token0 + && self.token1 == other.token1 + && self.fee == other.fee + && self.sqrt_ratio_x96 == other.sqrt_ratio_x96 + && self.liquidity == other.liquidity + && self.tick_current == other.tick_current + } } struct SwapState { @@ -121,33 +143,25 @@ impl

Pool

{ } /// Returns the current mid price of the pool in terms of token0, i.e. the ratio of token1 over token0 - pub fn token0_price(&mut self) -> Price { - self._token0_price.clone().unwrap_or_else(|| { - let sqrt_ratio_x96: BigUint = u256_to_big_uint(self.sqrt_ratio_x96); - let price = Price::new( - self.token0.clone(), - self.token1.clone(), - _Q192.clone(), - &sqrt_ratio_x96 * &sqrt_ratio_x96, - ); - self._token0_price = Some(price.clone()); - price - }) + pub fn token0_price(&self) -> Price { + let sqrt_ratio_x96: BigUint = u256_to_big_uint(self.sqrt_ratio_x96); + Price::new( + self.token0.clone(), + self.token1.clone(), + _Q192.clone(), + &sqrt_ratio_x96 * &sqrt_ratio_x96, + ) } /// Returns the current mid price of the pool in terms of token1, i.e. the ratio of token0 over token1 - pub fn token1_price(&mut self) -> Price { - self._token1_price.clone().unwrap_or_else(|| { - let sqrt_ratio_x96: BigUint = u256_to_big_uint(self.sqrt_ratio_x96); - let price = Price::new( - self.token1.clone(), - self.token0.clone(), - &sqrt_ratio_x96 * &sqrt_ratio_x96, - _Q192.clone(), - ); - self._token1_price = Some(price.clone()); - price - }) + pub fn token1_price(&self) -> Price { + let sqrt_ratio_x96: BigUint = u256_to_big_uint(self.sqrt_ratio_x96); + Price::new( + self.token1.clone(), + self.token0.clone(), + &sqrt_ratio_x96 * &sqrt_ratio_x96, + _Q192.clone(), + ) } /// Return the price of the given token in terms of the other token in the pool. @@ -157,7 +171,7 @@ impl

Pool

{ /// * `token`: The token to return price of /// /// returns: Price - pub fn price_of(&mut self, token: &Token) -> Price { + pub fn price_of(&self, token: &Token) -> Price { assert!(self.involves_token(token), "TOKEN"); if self.token0.equals(token) { self.token0_price() @@ -204,8 +218,6 @@ where liquidity, tick_current: get_tick_at_sqrt_ratio(sqrt_ratio_x96)?, tick_data_provider, - _token0_price: None, - _token1_price: None, }) } @@ -416,30 +428,6 @@ where } } -impl

fmt::Debug for Pool

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Pool") - .field("token0", &self.token0) - .field("token1", &self.token1) - .field("fee", &self.fee) - .field("sqrt_ratio_x96", &self.sqrt_ratio_x96) - .field("liquidity", &self.liquidity) - .field("tick_current", &self.tick_current) - .finish() - } -} - -impl

PartialEq for Pool

{ - fn eq(&self, other: &Self) -> bool { - self.token0 == other.token0 - && self.token1 == other.token1 - && self.fee == other.fee - && self.sqrt_ratio_x96 == other.sqrt_ratio_x96 - && self.liquidity == other.liquidity - && self.tick_current == other.tick_current - } -} - #[cfg(test)] mod tests { use super::*; @@ -542,7 +530,7 @@ mod tests { #[test] fn token0_price_returns_price_of_token0_in_terms_of_token1() -> Result<()> { - let mut pool = Pool::new( + let pool = Pool::new( USDC.clone(), DAI.clone(), FeeAmount::LOW, @@ -554,7 +542,7 @@ mod tests { .to_significant(5, Rounding::RoundHalfUp)?, "1.01" ); - let mut pool = Pool::new( + let pool = Pool::new( DAI.clone(), USDC.clone(), FeeAmount::LOW, @@ -571,7 +559,7 @@ mod tests { #[test] fn token1_price_returns_price_of_token1_in_terms_of_token0() -> Result<()> { - let mut pool = Pool::new( + let pool = Pool::new( USDC.clone(), DAI.clone(), FeeAmount::LOW, @@ -583,7 +571,7 @@ mod tests { .to_significant(5, Rounding::RoundHalfUp)?, "0.9901" ); - let mut pool = Pool::new( + let pool = Pool::new( DAI.clone(), USDC.clone(), FeeAmount::LOW, @@ -600,7 +588,7 @@ mod tests { #[test] fn price_of_returns_price_of_token_in_terms_of_other_token() { - let mut pool = Pool::new( + let pool = Pool::new( USDC.clone(), DAI.clone(), FeeAmount::LOW, @@ -615,7 +603,7 @@ mod tests { #[test] #[should_panic(expected = "TOKEN")] fn price_of_throws_if_invalid_token() { - let mut pool = Pool::new( + let pool = Pool::new( USDC.clone(), DAI.clone(), FeeAmount::LOW, diff --git a/src/entities/position.rs b/src/entities/position.rs index 1d9ac64..f84c9ad 100644 --- a/src/entities/position.rs +++ b/src/entities/position.rs @@ -16,6 +16,26 @@ pub struct Position

{ _mint_amounts: Option, } +impl

fmt::Debug for Position

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Position") + .field("pool", &self.pool) + .field("tick_lower", &self.tick_lower) + .field("tick_upper", &self.tick_upper) + .field("liquidity", &self.liquidity) + .finish() + } +} + +impl

PartialEq for Position

{ + fn eq(&self, other: &Self) -> bool { + self.pool == other.pool + && self.tick_lower == other.tick_lower + && self.tick_upper == other.tick_upper + && self.liquidity == other.liquidity + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MintAmounts { pub amount0: U256, @@ -145,7 +165,7 @@ impl

Position

{ /// ## Returns /// /// (sqrt_ratio_x96_lower, sqrt_ratio_x96_upper) - fn ratios_after_slippage(&mut self, slippage_tolerance: &Percent) -> (U256, U256) { + fn ratios_after_slippage(&self, slippage_tolerance: &Percent) -> (U256, U256) { let one = Percent::new(1, 1); let price_lower = self.pool.token0_price().as_fraction() * ((one.clone() - slippage_tolerance.clone()).as_fraction()); @@ -250,10 +270,7 @@ impl

Position

{ /// ## Returns /// /// The amounts, with slippage - pub fn burn_amounts_with_slippage( - &mut self, - slippage_tolerance: &Percent, - ) -> Result<(U256, U256)> { + pub fn burn_amounts_with_slippage(&self, slippage_tolerance: &Percent) -> Result<(U256, U256)> { // get lower/upper prices let (sqrt_ratio_x96_lower, sqrt_ratio_x96_upper) = self.ratios_after_slippage(slippage_tolerance); @@ -421,26 +438,6 @@ impl

Position

{ } } -impl

fmt::Debug for Position

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Position") - .field("pool", &self.pool) - .field("tick_lower", &self.tick_lower) - .field("tick_upper", &self.tick_upper) - .field("liquidity", &self.liquidity) - .finish() - } -} - -impl

PartialEq for Position

{ - fn eq(&self, other: &Self) -> bool { - self.pool == other.pool - && self.tick_lower == other.tick_lower - && self.tick_upper == other.tick_upper - && self.liquidity == other.liquidity - } -} - #[cfg(test)] mod tests { use super::*; @@ -704,7 +701,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_pool_at_min_price() { - let mut position = Position::new( + let position = Position::new( Pool::new(DAI.clone(), USDC.clone(), FeeAmount::LOW, MIN_SQRT_RATIO, 0).unwrap(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, @@ -720,7 +717,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_pool_at_max_price() { - let mut position = Position::new( + let position = Position::new( Pool::new( DAI.clone(), USDC.clone(), @@ -743,7 +740,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_below() { - let mut position = Position::new( + let position = Position::new( DAI_USDC_POOL.clone(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, @@ -759,7 +756,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_above() { - let mut position = Position::new( + let position = Position::new( DAI_USDC_POOL.clone(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, @@ -775,7 +772,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_within() { - let mut position = Position::new( + let position = Position::new( DAI_USDC_POOL.clone(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, @@ -791,7 +788,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_below_05_percent_slippage() { - let mut position = Position::new( + let position = Position::new( DAI_USDC_POOL.clone(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) + TICK_SPACING, @@ -807,7 +804,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_above_05_percent_slippage() { - let mut position = Position::new( + let position = Position::new( DAI_USDC_POOL.clone(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, @@ -823,7 +820,7 @@ mod tests { #[test] fn burn_amounts_with_slippage_is_correct_for_positions_within_05_percent_slippage() { - let mut position = Position::new( + let position = Position::new( DAI_USDC_POOL.clone(), 100e18 as u128, nearest_usable_tick(*POOL_TICK_CURRENT, TICK_SPACING) - TICK_SPACING * 2, diff --git a/src/entities/route.rs b/src/entities/route.rs index 86af5fc..3157c0a 100644 --- a/src/entities/route.rs +++ b/src/entities/route.rs @@ -82,7 +82,7 @@ impl Route price = self.pools[0].token1_price(); next_input = self.pools[0].token0.clone(); } - for pool in self.pools[1..].iter_mut() { + for pool in self.pools[1..].iter() { if next_input.equals(&pool.token0) { next_input = pool.token1.clone(); price = price.multiply(&pool.token0_price())?; diff --git a/src/lib.rs b/src/lib.rs index 792397a..cab071e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,25 @@ //! //! A Rust SDK for building applications on top of Uniswap V3. //! Migration from the TypeScript [Uniswap/v3-sdk](https://github.com/Uniswap/v3-sdk). +//! +//! ## Features +//! +//! - Opinionated Rust implementation of the Uniswap V3 SDK with a focus on readability and performance +//! - Usage of [alloy-rs](https://github.com/alloy-rs) types +//! - Reimplementation of the math libraries in [Uniswap V3 Math In Rust](https://github.com/0xKitsune/uniswap-v3-math) +//! based on optimizations presented in [Uni V3 Lib](https://github.com/Aperture-Finance/uni-v3-lib) +//! - Extensive unit tests and benchmarks +//! - An [`extensions`](./src/extensions) feature for additional functionalities related to Uniswap V3, including: +//! +//! - [`pool`](./src/extensions/pool.rs) module for creating a `Pool` struct from a pool key and fetching the +//! liquidity map within a tick range for the specified pool, using RPC client +//! - [`position`](./src/extensions/position.rs) module for creating a `Position` struct from a token id and fetching +//! the state and pool for all positions of the specified owner, using RPC client, etc +//! - [`price_tick_conversions`](./src/extensions/price_tick_conversions.rs) module for converting between prices and +//! ticks +//! - [`ephemeral_tick_data_provider`](./src/extensions/ephemeral_tick_data_provider.rs) module for fetching ticks using +//! an [ephemeral contract](https://github.com/Aperture-Finance/Aperture-Lens/blob/904101e4daed59e02fd4b758b98b0749e70b583b/contracts/EphemeralGetPopulatedTicksInRange.sol) +//! in a single `eth_call` pub mod abi; pub mod constants; diff --git a/src/nonfungible_position_manager.rs b/src/nonfungible_position_manager.rs index c2dd8dd..9c7bd4f 100644 --- a/src/nonfungible_position_manager.rs +++ b/src/nonfungible_position_manager.rs @@ -285,7 +285,7 @@ pub fn remove_call_parameters

( let token_id = options.token_id; // construct a partial position with a percentage of liquidity - let mut partial_position = Position::new( + let partial_position = Position::new( Pool::new( position.pool.token0.clone(), position.pool.token1.clone(), diff --git a/src/swap_router.rs b/src/swap_router.rs index 373539c..75458ae 100644 --- a/src/swap_router.rs +++ b/src/swap_router.rs @@ -220,9 +220,8 @@ pub fn swap_call_parameters> = Lazy::new(|| make_pool(TOKEN0.clone(), TOKEN1.clone())); diff --git a/src/tests.rs b/src/tests.rs index 7eeda03..f5bb96d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,8 +1,8 @@ #![cfg(test)] use crate::prelude::*; +use alloy_primitives::U256; use once_cell::sync::Lazy; -use ruint::aliases::U256; use uniswap_sdk_core::{prelude::*, token}; pub static ETHER: Lazy = Lazy::new(|| Ether::on_chain(1)); diff --git a/src/utils/compute_pool_address.rs b/src/utils/compute_pool_address.rs index 97b25bc..79d3dff 100644 --- a/src/utils/compute_pool_address.rs +++ b/src/utils/compute_pool_address.rs @@ -12,7 +12,38 @@ use alloy_sol_types::SolValue; /// * `fee`: The fee tier of the pool /// * `init_code_hash_manual_override`: Override the init code hash used to compute the pool address if necessary /// -/// returns: Address +/// ## Returns +/// +/// The computed pool address +/// +/// ## Examples +/// +/// ``` +/// use alloy_primitives::{address, Address}; +/// use uniswap_v3_sdk::prelude::*; +/// +/// const FACTORY_ADDRESS: Address = address!("1111111111111111111111111111111111111111"); +/// const USDC_ADDRESS: Address = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); +/// const DAI_ADDRESS: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F"); +/// let result = compute_pool_address( +/// FACTORY_ADDRESS, +/// USDC_ADDRESS, +/// DAI_ADDRESS, +/// FeeAmount::LOW, +/// None, +/// ); +/// assert_eq!(result, address!("90B1b09A9715CaDbFD9331b3A7652B24BfBEfD32")); +/// assert_eq!( +/// result, +/// compute_pool_address( +/// FACTORY_ADDRESS, +/// DAI_ADDRESS, +/// USDC_ADDRESS, +/// FeeAmount::LOW, +/// None, +/// ) +/// ); +/// ``` pub fn compute_pool_address( factory: Address, token_a: Address, @@ -32,35 +63,3 @@ pub fn compute_pool_address( init_code_hash_manual_override.unwrap_or(POOL_INIT_CODE_HASH), ) } - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::address; - - const FACTORY_ADDRESS: Address = address!("1111111111111111111111111111111111111111"); - const USDC_ADDRESS: Address = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - const DAI_ADDRESS: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F"); - - #[test] - fn test_compute_pool_address() { - let result = compute_pool_address( - FACTORY_ADDRESS, - USDC_ADDRESS, - DAI_ADDRESS, - FeeAmount::LOW, - None, - ); - assert_eq!(result, address!("90B1b09A9715CaDbFD9331b3A7652B24BfBEfD32")); - assert_eq!( - result, - compute_pool_address( - FACTORY_ADDRESS, - DAI_ADDRESS, - USDC_ADDRESS, - FeeAmount::LOW, - None, - ) - ); - } -}