Skip to content

Commit

Permalink
feat(ext): refactor Pool and Position functions (#81)
Browse files Browse the repository at this point in the history
Consolidate pool creation logic into `Pool` struct and introduce a tick data provider variant. Extend `Position` to support creation with a tick data provider. Add related tests and update documentation to reflect these changes.
  • Loading branch information
shuhuiluo authored Sep 15, 2024
1 parent 5b0ea90 commit 0110515
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 63 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "uniswap-v3-sdk"
version = "0.40.0"
version = "1.0.0-rc"
edition = "2021"
authors = ["Shuhui Luo <twitter.com/aureliano_law>"]
description = "Uniswap V3 SDK for Rust"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ It is feature-complete with unit tests matching the TypeScript SDK.
Add the following to your `Cargo.toml` file:

```toml
uniswap-v3-sdk = { version = "0.40.0", features = ["extensions", "std"] }
uniswap-v3-sdk = { version = "1.0.0-rc", features = ["extensions", "std"] }
```

### Usage
Expand Down
166 changes: 106 additions & 60 deletions src/extensions/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,68 +36,114 @@ where
)
}

/// Get a [`Pool`] struct from pool key
///
/// ## Arguments
///
/// * `chain_id`: The chain id
/// * `factory`: The factory address
/// * `token_a`: One of the tokens in the pool
/// * `token_b`: The other token in the pool
/// * `fee`: Fee tier of the pool
/// * `provider`: The alloy provider
/// * `block_id`: Optional block number to query.
#[inline]
pub async fn get_pool<T, P>(
chain_id: ChainId,
factory: Address,
token_a: Address,
token_b: Address,
fee: FeeAmount,
provider: P,
block_id: Option<BlockId>,
) -> Result<Pool, Error>
where
T: Transport + Clone,
P: Provider<T> + Clone,
{
let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest));
let pool_contract = get_pool_contract(factory, token_a, token_b, fee, provider.clone());
let token_a_contract = IERC20Metadata::new(token_a, provider.clone());
let token_b_contract = IERC20Metadata::new(token_b, provider.clone());
// TODO: use multicall
let slot_0 = pool_contract.slot0().block(block_id).call().await?;
let liquidity = pool_contract.liquidity().block(block_id).call().await?._0;
let token_a_decimals = token_a_contract.decimals().block(block_id).call().await?._0;
let token_a_name = token_a_contract.name().block(block_id).call().await?._0;
let token_a_symbol = token_a_contract.symbol().block(block_id).call().await?._0;
let token_b_decimals = token_b_contract.decimals().block(block_id).call().await?._0;
let token_b_name = token_b_contract.name().block(block_id).call().await?._0;
let token_b_symbol = token_b_contract.symbol().block(block_id).call().await?._0;
let sqrt_price_x96 = slot_0.sqrtPriceX96;
assert!(
!sqrt_price_x96.is_zero(),
"Pool has been created but not yet initialized"
);
Pool::new(
token!(
impl Pool {
/// Get a [`Pool`] struct from pool key
///
/// ## Arguments
///
/// * `chain_id`: The chain id
/// * `factory`: The factory address
/// * `token_a`: One of the tokens in the pool
/// * `token_b`: The other token in the pool
/// * `fee`: Fee tier of the pool
/// * `provider`: The alloy provider
/// * `block_id`: Optional block number to query.
#[inline]
pub async fn from_pool_key<T, P>(
chain_id: ChainId,
factory: Address,
token_a: Address,
token_b: Address,
fee: FeeAmount,
provider: P,
block_id: Option<BlockId>,
) -> Result<Self, Error>
where
T: Transport + Clone,
P: Provider<T> + Clone,
{
let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest));
let pool_contract = get_pool_contract(factory, token_a, token_b, fee, provider.clone());
let token_a_contract = IERC20Metadata::new(token_a, provider.clone());
let token_b_contract = IERC20Metadata::new(token_b, provider.clone());
// TODO: use multicall
let slot_0 = pool_contract.slot0().block(block_id).call().await?;
let liquidity = pool_contract.liquidity().block(block_id).call().await?._0;
let token_a_decimals = token_a_contract.decimals().block(block_id).call().await?._0;
let token_a_name = token_a_contract.name().block(block_id).call().await?._0;
let token_a_symbol = token_a_contract.symbol().block(block_id).call().await?._0;
let token_b_decimals = token_b_contract.decimals().block(block_id).call().await?._0;
let token_b_name = token_b_contract.name().block(block_id).call().await?._0;
let token_b_symbol = token_b_contract.symbol().block(block_id).call().await?._0;
let sqrt_price_x96 = slot_0.sqrtPriceX96;
assert!(
!sqrt_price_x96.is_zero(),
"Pool has been created but not yet initialized"
);
Self::new(
token!(
chain_id,
token_a,
token_a_decimals,
token_a_symbol,
token_a_name
),
token!(
chain_id,
token_b,
token_b_decimals,
token_b_symbol,
token_b_name
),
fee,
sqrt_price_x96,
liquidity,
)
}
}

impl<I: TickIndex> Pool<EphemeralTickMapDataProvider<I>> {
#[inline]
pub async fn from_pool_key_with_tick_data_provider<T, P>(
chain_id: ChainId,
factory: Address,
token_a: Address,
token_b: Address,
fee: FeeAmount,
provider: P,
block_id: Option<BlockId>,
) -> Result<Self, Error>
where
T: Transport + Clone,
P: Provider<T> + Clone,
{
let pool = Pool::from_pool_key(
chain_id,
factory,
token_a,
token_a_decimals,
token_a_symbol,
token_a_name
),
token!(
chain_id,
token_b,
token_b_decimals,
token_b_symbol,
token_b_name
),
fee,
sqrt_price_x96,
liquidity,
)
fee,
provider.clone(),
block_id,
)
.await?;
let tick_data_provider = EphemeralTickMapDataProvider::new(
pool.address(None, None),
provider.clone(),
None,
None,
block_id,
)
.await?;
Self::new_with_tick_data_provider(
pool.token0,
pool.token1,
pool.fee,
pool.sqrt_ratio_x96,
pool.liquidity,
tick_data_provider,
)
}
}

/// Normalizes the specified tick range.
Expand Down Expand Up @@ -215,7 +261,7 @@ mod tests {
use alloy_primitives::address;

async fn pool() -> Pool {
get_pool(
Pool::from_pool_key(
1,
FACTORY_ADDRESS,
address!("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"),
Expand Down
81 changes: 80 additions & 1 deletion src/extensions/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ where
liquidity,
..
} = position;
let pool = get_pool(
let pool = Pool::from_pool_key(
chain_id,
factory,
token0,
Expand Down Expand Up @@ -148,6 +148,66 @@ impl Position {
}
}

impl<I: TickIndex> Position<EphemeralTickMapDataProvider<I>> {
/// Get a [`Position`] struct from the token id with tick data provider in a single call
///
/// ## Arguments
///
/// * `chain_id`: The chain id
/// * `nonfungible_position_manager`: The nonfungible position manager address
/// * `token_id`: The token id
/// * `provider`: The alloy provider
/// * `block_id`: Optional block number to query
///
/// ## Returns
///
/// [`Position<EphemeralTickMapDataProvider<I>>`]
#[inline]
pub async fn from_token_id_with_tick_data_provider<T, P>(
chain_id: ChainId,
nonfungible_position_manager: Address,
token_id: U256,
provider: P,
block_id: Option<BlockId>,
) -> Result<Self, Error>
where
T: Transport + Clone,
P: Provider<T> + Clone,
{
let position = Position::from_token_id(
chain_id,
nonfungible_position_manager,
token_id,
provider.clone(),
block_id,
)
.await?;
let pool = position.pool;
let tick_data_provider = EphemeralTickMapDataProvider::new(
pool.address(None, None),
provider,
None,
None,
block_id,
)
.await?;
let pool = Pool::new_with_tick_data_provider(
pool.token0,
pool.token1,
pool.fee,
pool.sqrt_ratio_x96,
pool.liquidity,
tick_data_provider,
)?;
Ok(Position::new(
pool,
position.liquidity,
position.tick_lower.try_into().unwrap(),
position.tick_upper.try_into().unwrap(),
))
}
}

/// Get the state and pool for all positions of the specified owner by deploying an ephemeral
/// contract via `eth_call`.
///
Expand Down Expand Up @@ -438,6 +498,25 @@ mod tests {
assert_eq!(position.tick_upper, 264600);
}

#[tokio::test]
async fn test_from_token_id_with_tick_data_provider() {
let position = Position::from_token_id_with_tick_data_provider(
1,
NPM,
uint!(4_U256),
PROVIDER.clone(),
BLOCK_ID,
)
.await
.unwrap();
assert_eq!(position.liquidity, 34399999543676);
assert_eq!(position.tick_lower, 253320);
assert_eq!(position.tick_upper, 264600);
let tick = position.pool.tick_data_provider.get_tick(-92100).unwrap();
assert_eq!(tick.liquidity_gross, 456406095307);
assert_eq!(tick.liquidity_net, 456406095307);
}

#[tokio::test]
async fn test_get_all_positions_by_owner() {
let provider = PROVIDER.clone();
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
//! - [`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`.
//! - [`ephemeral_tick_map_data_provider`](./src/extensions/ephemeral_tick_map_data_provider.rs)
//! fetches ticks in a single `eth_call` and creates a `TickMap`
//! - [`tick_map`](./src/extensions/tick_map.rs) provides a way to access tick data directly
//! from a hashmap, supposedly more efficient than `TickList`
#![cfg_attr(not(any(feature = "std", test)), no_std)]
#![warn(
Expand Down

0 comments on commit 0110515

Please sign in to comment.