pool.rs•6.27 kB
//! Uniswap V3 pool operations
use crate::abis::{UniswapV3Factory, UniswapV3Pool, ERC20};
use crate::ethereum::utils;
use alloy::primitives::{Address, Uint};
use anyhow::{Context, Result};
use rust_decimal::Decimal;
/// Get the Uniswap V3 pool address for a token pair and fee tier
pub async fn get_uniswap_v3_pool_address<P: alloy::providers::Provider>(
token_a: Address,
token_b: Address,
fee: u64,
factory_address: Address,
provider: &P,
) -> Result<Address> {
let factory = UniswapV3Factory::new(factory_address, provider);
let fee_uint = Uint::from(fee);
// Try direct order
let pool = factory.getPool(token_a, token_b, fee_uint).call().await
.context("Failed to call getPool on UniswapV3Factory")?;
if pool != Address::ZERO {
return Ok(pool);
}
// Try reversed order just in case
let pool_reversed = factory.getPool(token_b, token_a, fee_uint).call().await
.context("Failed to call getPool (reversed) on UniswapV3Factory")?;
Ok(pool_reversed)
}
/// Get token0 and token1 addresses from a Uniswap V3 pool
pub async fn get_pool_tokens<P: alloy::providers::Provider>(
pool_address: Address,
provider: &P,
) -> Result<(Address, Address)> {
tracing::info!("Getting tokens from pool: {:?}", pool_address);
let pool = UniswapV3Pool::new(pool_address, provider);
// Get token addresses
let token0_result = pool.token0().call().await
.context("Failed to call token0() on pool")?;
let token1_result = pool.token1().call().await
.context("Failed to call token1() on pool")?;
let token0 = token0_result;
let token1 = token1_result;
Ok((token0, token1))
}
/// Get the current price from a Uniswap V3 pool by reading slot0
/// Returns the price as Decimal for token1/token0 (accounting for decimals)
pub async fn get_pool_price<P: alloy::providers::Provider>(
pool_address: Address,
provider: &P,
) -> Result<Decimal> {
tracing::info!("Getting price from pool: {:?}", pool_address);
let pool = UniswapV3Pool::new(pool_address, provider);
// Get slot0 data
let slot0_result = pool.slot0().call().await
.context("Failed to call slot0() on pool")?;
let sqrt_price_x96 = slot0_result.sqrtPriceX96;
// Get token addresses
let token0_result = pool.token0().call().await
.context("Failed to call token0() on pool")?;
let token1_result = pool.token1().call().await
.context("Failed to call token1() on pool")?;
let token0 = token0_result;
let token1 = token1_result;
// Get token decimals
let token0_decimals = get_token_decimals_helper(token0, provider).await.unwrap_or(18u8);
let token1_decimals = get_token_decimals_helper(token1, provider).await.unwrap_or(18u8);
// Convert sqrtPriceX96 to price
// Price = (sqrtPriceX96 / 2^96)^2
// With decimals: Price = (sqrtPriceX96 / 2^96)^2 * (10^token0_decimals / 10^token1_decimals)
// Calculate price using U256 first to avoid overflow, then convert to Decimal
// Price = (sqrtPriceX96 / 2^96)^2 * 10^(token0_decimals - token1_decimals)
use alloy::primitives::U256;
let q96 = U256::from(1u128 << 96);
let sqrt_price_u256 = U256::from(sqrt_price_x96);
// Use high precision scale factor (18 decimals)
let scale = U256::from(10u128.pow(18));
// Calculate ratio with high precision: (sqrt_price * scale) / q96
let ratio = (sqrt_price_u256 * scale) / q96;
// Convert to Decimal, then divide by scale to get the actual ratio
let ratio_dec = Decimal::from(ratio.to::<u128>()) / Decimal::from(10u128.pow(18));
// Square the ratio to get price
let mut price_dec = ratio_dec * ratio_dec;
// Apply decimal adjustment
let decimal_diff: i32 = (token0_decimals as i32) - (token1_decimals as i32);
if decimal_diff > 0 {
let adj = utils::power_of_10(decimal_diff);
price_dec *= adj;
} else if decimal_diff < 0 {
let adj = utils::power_of_10(-decimal_diff);
price_dec /= adj;
}
Ok(price_dec)
}
/// Calculate minimum amount out based on pool price, amount in, and slippage
/// Returns the minimum amount out with slippage tolerance applied
pub async fn calculate_min_amount_out<P: alloy::providers::Provider>(
pool_address: Address,
from_token: Address,
to_token: Address,
amount_in: Decimal,
slippage_percentage: Decimal,
provider: &P,
) -> Result<Decimal> {
tracing::info!("Calculating min amount out for swap: {:?} -> {:?}, amount_in: {}, slippage: {}%",
from_token, to_token, amount_in, slippage_percentage);
// Get pool price and token addresses
let pool_price = get_pool_price(pool_address, provider).await?;
let (token0, token1) = get_pool_tokens(pool_address, provider).await?;
// Determine the expected output amount based on token order in the pool
// get_pool_price returns token1/token0, so we need to adjust based on swap direction
let expected_amount_out = if from_token == token0 && to_token == token1 {
// Swapping token0 -> token1, so use price directly
amount_in * pool_price
} else if from_token == token1 && to_token == token0 {
// Swapping token1 -> token0, so use inverse price
amount_in / pool_price
} else {
return Err(anyhow::anyhow!(
"Token mismatch: from_token and to_token don't match pool structure (token0: {:?}, token1: {:?})",
token0, token1
));
};
// Apply slippage tolerance
let min_amount_out = expected_amount_out * (Decimal::from(1) - slippage_percentage / Decimal::from(100));
tracing::info!("Calculated min amount out: {} (expected: {}, slippage: {}%)",
min_amount_out, expected_amount_out, slippage_percentage);
Ok(min_amount_out)
}
/// Helper function to get token decimals
async fn get_token_decimals_helper<P: alloy::providers::Provider>(
token_address: Address,
provider: &P,
) -> Result<u8> {
let erc20 = ERC20::new(token_address, provider);
let decimals_result = erc20.decimals().call().await
.context("Failed to call decimals()")?;
Ok(decimals_result)
}