diff --git a/Cargo.toml b/Cargo.toml index 1eb3162..1699a9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uniswap-v3-sdk" -version = "0.39.0" +version = "0.40.0" edition = "2021" authors = ["Shuhui Luo "] description = "Uniswap V3 SDK for Rust" diff --git a/src/entities/tick.rs b/src/entities/tick.rs index 786638f..5540256 100644 --- a/src/entities/tick.rs +++ b/src/entities/tick.rs @@ -2,7 +2,8 @@ use crate::prelude::*; use alloy_primitives::{aliases::I24, Signed}; use core::{ fmt::Debug, - ops::{Add, Div, Mul, Rem, Shl, Shr, Sub}, + hash::Hash, + ops::{Add, BitAnd, Div, Mul, Rem, Shl, Shr, Sub}, }; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -34,7 +35,9 @@ pub trait TickIndex: Copy + Debug + Default + + Hash + Ord + + BitAnd + Add + Div + Mul @@ -66,6 +69,14 @@ pub trait TickIndex: self / tick_spacing } } + + #[inline] + fn position(self) -> (Self, u8) { + ( + self >> 8, + (self & Self::try_from(0xff).unwrap()).try_into().unwrap() as u8, + ) + } } impl TickIndex for i32 { diff --git a/src/entities/tick_list_data_provider.rs b/src/entities/tick_list_data_provider.rs index f2c1773..2c66daf 100644 --- a/src/entities/tick_list_data_provider.rs +++ b/src/entities/tick_list_data_provider.rs @@ -39,7 +39,7 @@ mod tests { use once_cell::sync::Lazy; static PROVIDER: Lazy = - Lazy::new(|| TickListDataProvider::new(vec![Tick::new(-1, 1, -1), Tick::new(1, 1, 1)], 1)); + Lazy::new(|| TickListDataProvider::new(vec![Tick::new(-1, 1, 1), Tick::new(1, 1, -1)], 1)); #[test] fn can_take_an_empty_list_of_ticks() { @@ -67,14 +67,14 @@ mod tests { #[test] fn gets_the_smallest_tick_from_the_list() { let tick = PROVIDER.get_tick(-1).unwrap(); - assert_eq!(tick.liquidity_net, -1); + assert_eq!(tick.liquidity_net, 1); assert_eq!(tick.liquidity_gross, 1); } #[test] fn gets_the_largest_tick_from_the_list() { let tick = PROVIDER.get_tick(1).unwrap(); - assert_eq!(tick.liquidity_net, 1); + assert_eq!(tick.liquidity_net, -1); assert_eq!(tick.liquidity_gross, 1); } } diff --git a/src/extensions/ephemeral_tick_data_provider.rs b/src/extensions/ephemeral_tick_data_provider.rs index e9f5ca8..9366dac 100644 --- a/src/extensions/ephemeral_tick_data_provider.rs +++ b/src/extensions/ephemeral_tick_data_provider.rs @@ -12,10 +12,9 @@ pub struct EphemeralTickDataProvider { pub pool: Address, pub tick_lower: I, pub tick_upper: I, + pub tick_spacing: I, pub block_id: Option, pub ticks: Vec>, - /// the minimum distance between two ticks in the list - pub tick_spacing: I, } impl EphemeralTickDataProvider { @@ -52,9 +51,9 @@ impl EphemeralTickDataProvider { pool, tick_lower: I::from_i24(tick_lower), tick_upper: I::from_i24(tick_upper), + tick_spacing: I::from_i24(tick_spacing), block_id, ticks, - tick_spacing: I::from_i24(tick_spacing), }) } } @@ -111,16 +110,16 @@ mod tests { let tick = provider.get_tick(-92110)?; assert_eq!(tick.liquidity_gross, 398290794261); assert_eq!(tick.liquidity_net, 398290794261); - let (tick, success) = provider.next_initialized_tick_within_one_word( - MIN_TICK.as_i32() + TICK_SPACING, + let (tick, initialized) = provider.next_initialized_tick_within_one_word( + MIN_TICK_I32 + TICK_SPACING, true, TICK_SPACING, )?; - assert!(success); + assert!(initialized); assert_eq!(tick, -887270); - let (tick, success) = + let (tick, initialized) = provider.next_initialized_tick_within_one_word(0, false, TICK_SPACING)?; - assert!(success); + assert!(initialized); assert_eq!(tick, 100); let provider: TickListDataProvider = provider.into(); let tick = provider.get_tick(-92110)?; diff --git a/src/extensions/ephemeral_tick_map_data_provider.rs b/src/extensions/ephemeral_tick_map_data_provider.rs index 37f8ee3..52b176c 100644 --- a/src/extensions/ephemeral_tick_map_data_provider.rs +++ b/src/extensions/ephemeral_tick_map_data_provider.rs @@ -1,22 +1,19 @@ //! ## Ephemeral Tick Map Data Provider //! A data provider that fetches ticks using an [ephemeral contract](https://github.com/Aperture-Finance/Aperture-Lens/blob/904101e4daed59e02fd4b758b98b0749e70b583b/contracts/EphemeralGetPopulatedTicksInRange.sol) in a single `eth_call`. -#![allow(unused_variables)] use crate::prelude::*; use alloy::{eips::BlockId, providers::Provider, transports::Transport}; use alloy_primitives::{aliases::I24, Address}; -use uniswap_lens::pool_lens; /// A data provider that fetches ticks using an ephemeral contract in a single `eth_call`. #[derive(Clone, Debug, PartialEq)] -pub struct EphemeralTickMapDataProvider { +pub struct EphemeralTickMapDataProvider { pub pool: Address, pub tick_lower: I, pub tick_upper: I, - pub block_id: Option, - pub tick_map: TickMap, - /// the minimum distance between two ticks in the list pub tick_spacing: I, + pub block_id: Option, + pub tick_map: TickMap, } impl EphemeralTickMapDataProvider { @@ -32,14 +29,17 @@ impl EphemeralTickMapDataProvider { T: Transport + Clone, P: Provider, { - let tick_lower = tick_lower.map_or(MIN_TICK, I::to_i24); - let tick_upper = tick_upper.map_or(MAX_TICK, I::to_i24); - let (ticks, tick_spacing) = pool_lens::get_populated_ticks_in_range( - pool, tick_lower, tick_upper, provider, block_id, - ) - .await - .map_err(Error::LensError)?; - unimplemented!() + let provider = + EphemeralTickDataProvider::new(pool, provider, tick_lower, tick_upper, block_id) + .await?; + Ok(Self { + pool, + tick_lower: provider.tick_lower, + tick_upper: provider.tick_upper, + tick_spacing: provider.tick_spacing, + block_id, + tick_map: TickMap::new(provider.ticks, provider.tick_spacing), + }) } } @@ -48,7 +48,7 @@ impl TickDataProvider for EphemeralTickMapDataProvider { #[inline] fn get_tick(&self, tick: I) -> Result<&Tick, Error> { - unimplemented!() + self.tick_map.get_tick(tick) } #[inline] @@ -58,6 +58,43 @@ impl TickDataProvider for EphemeralTickMapDataProvider { lte: bool, tick_spacing: I, ) -> Result<(I, bool), Error> { - unimplemented!() + self.tick_map + .next_initialized_tick_within_one_word(tick, lte, tick_spacing) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + use alloy_primitives::address; + + const TICK_SPACING: i32 = 10; + + #[tokio::test] + async fn test_ephemeral_tick_map_data_provider() -> Result<(), Error> { + let provider = EphemeralTickMapDataProvider::new( + address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), + PROVIDER.clone(), + None, + None, + *BLOCK_ID, + ) + .await?; + let tick = provider.get_tick(-92110)?; + assert_eq!(tick.liquidity_gross, 398290794261); + assert_eq!(tick.liquidity_net, 398290794261); + let (tick, initialized) = provider.next_initialized_tick_within_one_word( + MIN_TICK_I32 + TICK_SPACING, + true, + TICK_SPACING, + )?; + assert_eq!(tick, -887270); + assert!(initialized); + let (tick, initialized) = + provider.next_initialized_tick_within_one_word(0, false, TICK_SPACING)?; + assert!(initialized); + assert_eq!(tick, 100); + Ok(()) } } diff --git a/src/extensions/pool.rs b/src/extensions/pool.rs index 79bc316..8dfaa17 100644 --- a/src/extensions/pool.rs +++ b/src/extensions/pool.rs @@ -10,7 +10,6 @@ use alloy::{ transports::Transport, }; use alloy_primitives::{Address, ChainId, B256}; -use anyhow::Result; use uniswap_lens::{ bindings::{ ierc20metadata::IERC20Metadata, iuniswapv3pool::IUniswapV3Pool::IUniswapV3PoolInstance, diff --git a/src/extensions/tick_map.rs b/src/extensions/tick_map.rs index 8343475..93a9ed9 100644 --- a/src/extensions/tick_map.rs +++ b/src/extensions/tick_map.rs @@ -1,32 +1,88 @@ //! ## Tick Map -//! [`TickMapTrait`] is a trait that provides a way to access tick data directly from a hashmap, -//! supposedly more efficient than [`TickList`]. +//! [`TickMap`] provides a way to access tick data directly from a hashmap, supposedly more +//! efficient than [`TickList`]. use crate::prelude::*; -use alloy_primitives::U256; -use anyhow::Result; +use alloy::uint; +use alloy_primitives::{aliases::I24, U256}; use rustc_hash::FxHashMap; #[derive(Clone, Debug, PartialEq)] -pub struct TickMap { - pub bitmap: FxHashMap, - pub ticks: FxHashMap, +pub struct TickMap { + pub bitmap: FxHashMap, + pub inner: FxHashMap>, + pub tick_spacing: I, } -pub trait TickMapTrait { - type Tick; - +impl TickMap { #[inline] #[must_use] - fn position(tick: i32) -> (i16, u8) { - ((tick >> 8) as i16, (tick & 0xff) as u8) + pub fn new(ticks: Vec>, tick_spacing: I) -> Self { + ticks.validate_list(tick_spacing); + let mut bitmap = FxHashMap::::default(); + for tick in &ticks { + let compressed = tick.index.compress(tick_spacing); + let (word_pos, bit_pos) = compressed.position(); + let word = bitmap.get(&word_pos).unwrap_or(&U256::ZERO); + bitmap.insert(word_pos, word | uint!(1_U256) << bit_pos); + } + Self { + bitmap, + inner: FxHashMap::from_iter(ticks.into_iter().map(|tick| (tick.index, tick))), + tick_spacing, + } } - - fn get_bitmap(&self, tick: i32) -> Result; - - fn get_tick(&self, index: i32) -> &Self::Tick; } -// impl TickMapTrait for TickMap {} +impl TickDataProvider for TickMap { + type Index = I; -pub trait TickMapDataProvider: TickMapTrait + TickDataProvider {} + #[inline] + fn get_tick(&self, tick: Self::Index) -> Result<&Tick, Error> { + self.inner + .get(&tick) + .ok_or(Error::InvalidTick(tick.to_i24())) + } + + #[inline] + fn next_initialized_tick_within_one_word( + &self, + tick: Self::Index, + lte: bool, + tick_spacing: Self::Index, + ) -> Result<(Self::Index, bool), Error> { + let compressed = tick.compress(tick_spacing); + if lte { + let (word_pos, bit_pos) = compressed.position(); + // all the 1s at or to the right of the current `bit_pos` + // (2 << bitPos) may overflow but fine since 2 << 255 = 0 + let mask = (TWO << bit_pos) - uint!(1_U256); + let word = self.bitmap.get(&word_pos).unwrap_or(&U256::ZERO); + let masked = word & mask; + let initialized = masked != U256::ZERO; + let bit_pos = if initialized { + let msb = masked.most_significant_bit() as u8; + (bit_pos - msb) as i32 + } else { + bit_pos as i32 + }; + let next = (compressed - Self::Index::try_from(bit_pos).unwrap()) * tick_spacing; + Ok((next, initialized)) + } else { + let (word_pos, bit_pos) = compressed.position(); + // all the 1s at or to the left of the `bit_pos` + let mask = U256::ZERO - (uint!(1_U256) << bit_pos); + let word = self.bitmap.get(&word_pos).unwrap_or(&U256::ZERO); + let masked = word & mask; + let initialized = masked != U256::ZERO; + let bit_pos = if initialized { + let lsb = masked.least_significant_bit() as u8; + (lsb - bit_pos) as i32 + } else { + (255 - bit_pos) as i32 + }; + let next = (compressed + Self::Index::try_from(bit_pos).unwrap()) * tick_spacing; + Ok((next, initialized)) + } + } +} diff --git a/src/utils/tick_list.rs b/src/utils/tick_list.rs index 9d73abb..00db821 100644 --- a/src/utils/tick_list.rs +++ b/src/utils/tick_list.rs @@ -69,7 +69,9 @@ impl TickList for [Tick] { assert!(self[i] >= self[i - 1], "SORTED"); } assert_eq!( - self.iter().fold(0, |acc, x| acc + x.liquidity_net), + self.iter().fold(0_u128, |acc, x| acc + .checked_add_signed(x.liquidity_net) + .expect("ZERO_NET")), 0, "ZERO_NET" );