use alloy::{
eips::BlockId,
network::Ethereum,
primitives::{aliases::I24, Address, TxHash, U256},
providers::DynProvider,
};
use anyhow::Result;
use async_trait::async_trait;
use tracing::{debug, info, instrument};
use uniswap_sdk_core::{
entities::BaseCurrency,
prelude::{Currency, CurrencyAmount, Price, ToBig},
};
use crate::{
impl_pool_base, impl_position_viewer_helpers, impl_token_helper, impl_v3_pool_infuser,
impl_v3_pool_state, impl_v3_pool_viewer,
pool_makers::common::{can_fulfill_order_range, can_place_order_range},
traits::{
pool_infuser::{
AddLiquidityOptions, AddLiquidityParams, AddLiquiditySpecificOptions,
MintSpecificOptions, PoolInfuser, RemoveLiquidityOptions, RemoveLiquidityParams,
},
pool_maker::{
CancelBatchOrdersResult, CancelOrderResult, CancelOrderResultItem,
FulfillBatchOrdersResult, FulfillOrderOptions, FulfillOrderParams, FulfillOrderResult,
OrderInfo, OrderMetadata, OrderPriceRange, OrderSide, PlaceBatchOrdersResult,
PlaceOrderOptions, PlaceOrderParams, PlaceOrderResult, PoolMaker, PositionInfo,
PriceCheckInput, PriceCheckResult, PricePlaceableResult, RestrictedPriceRange,
},
pool_state::PoolState,
pool_viewer::PoolViewer,
},
types::v3_pool_key::V3PoolKey,
};
#[derive(Clone, Debug)]
pub struct V3PoolMaker {
pub pool_key: V3PoolKey,
pub pool_address: Address,
pub position_manager_address: Address,
pub sender_address: Address,
pub tick_spacing: i32,
pub order_tick_count: i32,
pub is_token_a_base: bool,
pub provider: DynProvider<Ethereum>,
}
impl V3PoolMaker {
#[instrument(skip(pool_key), fields(
pool_address = ?pool_address,
position_manager_address = ?position_manager_address,
token_a = ?pool_key.token_a.address(),
token_b = ?pool_key.token_b.address(),
fee = ?pool_key.fee,
sender_address = ?sender_address,
is_token_a_base = is_token_a_base
))]
pub fn new(
pool_key: V3PoolKey,
pool_address: Address,
position_manager_address: Address,
sender_address: Address,
tick_spacing: i32,
is_token_a_base: bool,
provider: DynProvider<Ethereum>,
) -> Self {
Self::new_with_order_tick_count(
pool_key,
pool_address,
position_manager_address,
sender_address,
tick_spacing,
2, is_token_a_base,
provider,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_order_tick_count(
pool_key: V3PoolKey,
pool_address: Address,
position_manager_address: Address,
sender_address: Address,
tick_spacing: i32,
order_tick_count: i32,
is_token_a_base: bool,
provider: DynProvider<Ethereum>,
) -> Self {
debug!(
pool_address = ?pool_address,
position_manager_address = ?position_manager_address,
tick_spacing = tick_spacing,
order_tick_count = order_tick_count,
is_token_a_base = is_token_a_base,
"Creating V3PoolMaker"
);
Self {
pool_key,
pool_address,
position_manager_address,
sender_address,
tick_spacing,
order_tick_count,
is_token_a_base,
provider,
}
}
pub fn is_token_a_base(&self) -> bool { self.is_token_a_base }
pub fn sender_address(&self) -> Address { self.sender_address }
pub fn pool_address(&self) -> Address { self.pool_address }
pub fn position_manager_address(&self) -> Address { self.position_manager_address }
pub fn pool_key(&self) -> &V3PoolKey { &self.pool_key }
pub fn tick_spacing(&self) -> i32 { self.tick_spacing }
pub fn order_tick_count(&self) -> i32 { self.order_tick_count }
pub fn get_order_tick_range(&self, order_tick: i32, side: OrderSide) -> Result<(I24, I24)> {
let rounded_tick = (order_tick / self.tick_spacing) * self.tick_spacing;
let rounded_tick_i24 = I24::try_from(rounded_tick)?;
let order_tick_span = self.tick_spacing * self.order_tick_count;
let half_span = order_tick_span / 2;
let half_span_i24 = I24::try_from(half_span)?;
let tick_lower = rounded_tick_i24 - half_span_i24;
let tick_upper = rounded_tick_i24 + half_span_i24;
match side {
OrderSide::Buy => Ok((tick_lower, tick_upper)),
OrderSide::Sell => Ok((tick_lower, tick_upper)),
}
}
fn get_order_tick_range_checked(
&self,
current_tick: i32,
order_tick: i32,
side: OrderSide,
) -> Result<(I24, I24)> {
let restricted_range = self.get_restricted_tick_range(current_tick)?;
let (tick_lower, tick_upper) = self.get_order_tick_range(order_tick, side)?;
let tick_lower_i32 = tick_lower.as_i32();
let tick_upper_i32 = tick_upper.as_i32();
if !self.can_place_order_range(&restricted_range, tick_lower_i32, tick_upper_i32, side) {
return Err(anyhow::anyhow!(
"Order range [{}, {}) cannot be placed: it overlaps with restricted range [{}, \
{}) for {:?} order",
tick_lower_i32,
tick_upper_i32,
restricted_range.tick_lower,
restricted_range.tick_upper,
side
));
}
Ok((tick_lower, tick_upper))
}
pub fn can_fulfill_order(
&self,
current_tick: i32,
tick_lower: i32,
tick_upper: i32,
order_side: OrderSide,
) -> Result<bool> {
let restricted_range = self.get_restricted_tick_range(current_tick)?;
Ok(self.can_fulfill_order_range(&restricted_range, tick_lower, tick_upper, order_side))
}
pub fn can_fulfill_order_range(
&self,
restricted_range: &RestrictedPriceRange,
tick_lower: i32,
tick_upper: i32,
order_side: OrderSide,
) -> bool {
can_fulfill_order_range(
restricted_range,
tick_lower,
tick_upper,
order_side,
self.is_token_a_base,
)
}
pub fn can_place_order_range(
&self,
restricted_range: &RestrictedPriceRange,
tick_lower: i32,
tick_upper: i32,
order_side: OrderSide,
) -> bool {
can_place_order_range(
restricted_range,
tick_lower,
tick_upper,
order_side,
self.is_token_a_base,
)
}
fn get_restricted_tick_range(&self, current_tick: i32) -> Result<RestrictedPriceRange> {
let tick_lower = (current_tick / self.tick_spacing) * self.tick_spacing - self.tick_spacing;
let tick_upper = tick_lower + 2 * self.tick_spacing;
let lower_price_i24 = I24::try_from(tick_lower)?;
let upper_price_i24 = I24::try_from(tick_upper)?;
let lower_price = self.tick_to_base_price(lower_price_i24)?;
let upper_price = self.tick_to_base_price(upper_price_i24)?;
Ok(RestrictedPriceRange { lower_price, upper_price, tick_lower, tick_upper })
}
pub fn tick_to_base_price(&self, tick: I24) -> Result<Price<Currency, Currency>> {
let price = self.tick_to_price(tick)?;
if self.is_token_a_base {
Ok(price)
} else {
Ok(price.invert())
}
}
pub fn base_price_to_closest_tick(&self, price: &Price<Currency, Currency>) -> Result<I24> {
let pool_price = if self.is_token_a_base {
price.clone()
} else {
price.invert()
};
self.price_to_closest_tick(pool_price)
}
pub fn get_order_amounts(
&self,
base_amount: &CurrencyAmount<Currency>,
price: &Price<Currency, Currency>,
side: OrderSide,
) -> Result<(CurrencyAmount<Currency>, CurrencyAmount<Currency>)> {
let quote_amount = price.quote(base_amount)?;
match (self.is_token_a_base, side) {
(true, OrderSide::Buy) => Ok((
CurrencyAmount::from_raw_amount(self.pool_key.token_a.clone(), 0)?,
quote_amount,
)),
(true, OrderSide::Sell) => Ok((
base_amount.clone(),
CurrencyAmount::from_raw_amount(self.pool_key.token_b.clone(), 0)?,
)),
(false, OrderSide::Buy) => Ok((
quote_amount,
CurrencyAmount::from_raw_amount(self.pool_key.token_b.clone(), 0)?,
)),
(false, OrderSide::Sell) => Ok((
CurrencyAmount::from_raw_amount(self.pool_key.token_a.clone(), 0)?,
base_amount.clone(),
)),
}
}
#[allow(clippy::too_many_arguments)]
fn calculate_position_info(
&self,
token_id: U256,
tick_lower: i32,
tick_upper: i32,
liquidity: u128,
tokens_owed0: u128,
tokens_owed1: u128,
sqrt_price_x96: alloy::primitives::aliases::U160,
) -> Result<PositionInfo> {
use waterpump_evm_amm_math::{
sqrt_price_math::{get_amount_0_delta, get_amount_1_delta},
tick_math::{get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio},
};
let sqrt_ratio_lower = get_sqrt_ratio_at_tick(tick_lower)?;
let sqrt_ratio_upper = get_sqrt_ratio_at_tick(tick_upper)?;
let sqrt_price_u256 = U256::from(sqrt_price_x96);
let current_tick = get_tick_at_sqrt_ratio(sqrt_price_u256)?;
let (amount0_raw, amount1_raw) = if current_tick < tick_lower {
let amount0 = get_amount_0_delta(sqrt_ratio_lower, sqrt_ratio_upper, liquidity, true)?;
(amount0, U256::ZERO)
} else if current_tick < tick_upper {
let amount0 = get_amount_0_delta(sqrt_price_u256, sqrt_ratio_upper, liquidity, true)?;
let amount1 = get_amount_1_delta(sqrt_ratio_lower, sqrt_price_u256, liquidity, true)?;
(amount0, amount1)
} else {
let amount1 = get_amount_1_delta(sqrt_ratio_lower, sqrt_ratio_upper, liquidity, true)?;
(U256::ZERO, amount1)
};
let amount0 = CurrencyAmount::from_raw_amount(
self.pool_key.token_a.clone(),
amount0_raw.to_big_int(),
)?;
let amount1 = CurrencyAmount::from_raw_amount(
self.pool_key.token_b.clone(),
amount1_raw.to_big_int(),
)?;
let fees_owed0 = CurrencyAmount::from_raw_amount(
self.pool_key.token_a.clone(),
U256::from(tokens_owed0).to_big_int(),
)?;
let fees_owed1 = CurrencyAmount::from_raw_amount(
self.pool_key.token_b.clone(),
U256::from(tokens_owed1).to_big_int(),
)?;
Ok(PositionInfo {
token_id,
tick_lower,
tick_upper,
liquidity,
amount0,
amount1,
fees_owed0,
fees_owed1,
})
}
}
impl_pool_base!(V3PoolMaker);
impl_token_helper!(V3PoolMaker);
impl_v3_pool_viewer!(V3PoolMaker);
impl_v3_pool_state!(V3PoolMaker);
impl_v3_pool_infuser!(V3PoolMaker);
impl_position_viewer_helpers!(V3PoolMaker);
#[async_trait]
impl PoolMaker for V3PoolMaker {
fn base_currency(&self) -> Currency {
if self.is_token_a_base {
self.pool_key.token_a.clone()
} else {
self.pool_key.token_b.clone()
}
}
fn quote_currency(&self) -> Currency {
if self.is_token_a_base {
self.pool_key.token_b.clone()
} else {
self.pool_key.token_a.clone()
}
}
#[instrument(skip(self, params, options), fields(
position_manager_address = ?self.position_manager_address(),
side = ?params.side,
amount = ?params.base_amount
))]
async fn place_order(
&self,
params: PlaceOrderParams,
options: PlaceOrderOptions,
) -> Result<PlaceOrderResult> {
let currency0_price = PoolViewer::currency0_price(self, None).await?;
let current_tick = self.price_to_closest_tick(currency0_price.clone())?.as_i32();
let order_tick = self.base_price_to_closest_tick(¶ms.price)?.as_i32();
let (tick_lower, tick_upper) =
self.get_order_tick_range_checked(current_tick, order_tick, params.side)?;
let (amount0, amount1) =
self.get_order_amounts(¶ms.base_amount, ¶ms.price, params.side)?;
let add_liquidity_params = AddLiquidityParams {
amount0,
amount1,
tick_upper,
tick_lower,
token0_price: currency0_price,
specific_opts: AddLiquiditySpecificOptions::Mint(MintSpecificOptions {
recipient: params.recipient,
create_pool: false,
}),
};
let add_liquidity_options = AddLiquidityOptions {
slippage_tolerance: options.slippage_tolerance,
deadline: params.deadline,
use_native: None,
token0_permit: None,
token1_permit: None,
};
let result =
PoolInfuser::add_liquidity(self, add_liquidity_params, add_liquidity_options).await?;
let timestamp = chrono::Utc::now().timestamp();
Ok(PlaceOrderResult {
token_id: result.token_id,
tx_hash: result.tx_hash,
price: params.price,
base_amount: params.base_amount,
side: params.side,
tick_lower: tick_lower.as_i32(),
tick_upper: tick_upper.as_i32(),
timestamp,
})
}
#[instrument(skip(self, params, options), fields(
position_manager_address = ?self.position_manager_address(),
num_orders = params.items.len()
))]
async fn place_batch_orders(
&self,
params: crate::traits::pool_maker::PlaceBatchOrdersParams,
options: PlaceOrderOptions,
) -> Result<PlaceBatchOrdersResult> {
use crate::traits::{
pool_infuser::{AddBatchLiquidityItem, AddBatchLiquidityParams, AddLiquidityOptions},
pool_maker::PlaceOrderResultItem,
};
if params.items.is_empty() {
return Ok(PlaceBatchOrdersResult {
tx_hash: TxHash::default(),
orders: Vec::new(),
timestamp: chrono::Utc::now().timestamp(),
});
}
let currency0_price = PoolViewer::currency0_price(self, None).await?;
let current_tick = self.price_to_closest_tick(currency0_price.clone())?.as_i32();
let mut batch_items = Vec::with_capacity(params.items.len());
let mut tick_ranges = Vec::with_capacity(params.items.len());
for item in ¶ms.items {
let order_tick = self.base_price_to_closest_tick(&item.price)?.as_i32();
let (tick_lower, tick_upper) =
self.get_order_tick_range_checked(current_tick, order_tick, item.side)?;
tick_ranges.push((tick_lower.as_i32(), tick_upper.as_i32()));
let (amount0, amount1) =
self.get_order_amounts(&item.base_amount, &item.price, item.side)?;
batch_items.push(AddBatchLiquidityItem {
amount0,
amount1,
tick_upper,
tick_lower,
specific_opts: AddLiquiditySpecificOptions::Mint(MintSpecificOptions {
recipient: params.recipient,
create_pool: false,
}),
});
}
let batch_params =
AddBatchLiquidityParams { token0_price: currency0_price, items: batch_items };
let batch_options = AddLiquidityOptions {
slippage_tolerance: options.slippage_tolerance,
deadline: params.deadline,
use_native: None,
token0_permit: None,
token1_permit: None,
};
let result = PoolInfuser::add_batch_liquidity(self, batch_params, batch_options).await?;
let timestamp = chrono::Utc::now().timestamp();
let orders: Vec<PlaceOrderResultItem> = result
.results
.into_iter()
.zip(params.items.iter())
.zip(tick_ranges.iter())
.map(|((add_result, item), (tick_lower, tick_upper))| PlaceOrderResultItem {
token_id: add_result.token_id,
price: item.price.clone(),
base_amount: item.base_amount.clone(),
side: item.side,
tick_lower: *tick_lower,
tick_upper: *tick_upper,
})
.collect();
Ok(PlaceBatchOrdersResult { tx_hash: result.tx_hash, orders, timestamp })
}
#[instrument(skip(self, params, options), fields(
position_manager_address = ?self.position_manager_address(),
token_id = ?params.order_metadata.token_id,
order_side = ?params.order_metadata.order_side
))]
async fn fulfill_order(
&self,
params: FulfillOrderParams,
options: FulfillOrderOptions,
) -> Result<FulfillOrderResult> {
let currency0_price = PoolViewer::currency0_price(self, None).await?;
let position = PoolMaker::get_position(self, params.order_metadata.token_id, None).await?;
debug!(
token_id = ?params.order_metadata.token_id,
liquidity = ?position.liquidity,
tick_lower = ?position.tick_lower,
tick_upper = ?position.tick_upper,
"Position data retrieved for fulfill order"
);
let current_tick = self.price_to_closest_tick(currency0_price.clone())?.as_i32();
if !self.can_fulfill_order(
current_tick,
position.tick_lower,
position.tick_upper,
params.order_metadata.order_side,
)? {
return Err(anyhow::anyhow!(
"Order cannot be fulfilled yet. Current tick {} has not crossed through position \
range [{}, {}) for {:?} order. Buy orders require price to drop below, sell \
orders require price to rise above.",
current_tick,
position.tick_lower,
position.tick_upper,
params.order_metadata.order_side
));
}
let tick_lower_i24 = I24::try_from(position.tick_lower)?;
let tick_upper_i24 = I24::try_from(position.tick_upper)?;
let remove_params = RemoveLiquidityParams {
token_id: params.order_metadata.token_id,
liquidity: U256::from(position.liquidity),
recipient: params.recipient,
deadline: params.deadline,
token0_price: currency0_price.clone(),
tick_lower: tick_lower_i24,
tick_upper: tick_upper_i24,
};
let remove_options = RemoveLiquidityOptions {
slippage_tolerance: options.slippage_tolerance,
burn_token: true,
permit: None,
collect_options: Default::default(),
};
let result = PoolInfuser::remove_liquidity(self, remove_params, remove_options).await?;
let timestamp = chrono::Utc::now().timestamp();
let (base_amount, quote_amount) = if self.is_token_a_base {
(result.amount0, result.amount1)
} else {
(result.amount1, result.amount0)
};
let fulfill_price = self.tick_to_base_price(tick_lower_i24)?;
Ok(FulfillOrderResult {
token_id: params.order_metadata.token_id,
tx_hash: result.tx_hash,
fully_filled: true,
base_amount,
quote_amount,
fill_price: fulfill_price,
fee: None,
tick_lower: position.tick_lower,
tick_upper: position.tick_upper,
timestamp,
})
}
#[instrument(skip(self, params, options), fields(
position_manager_address = ?self.position_manager_address(),
num_orders = params.items.len()
))]
async fn fulfill_batch_orders(
&self,
params: crate::traits::pool_maker::FulfillBatchOrdersParams,
options: FulfillOrderOptions,
) -> Result<FulfillBatchOrdersResult> {
use crate::traits::{
pool_infuser::{
RemoveBatchLiquidityItem, RemoveBatchLiquidityParams, RemoveLiquidityOptions,
},
pool_maker::FulfillOrderResultItem,
};
if params.items.is_empty() {
return Ok(FulfillBatchOrdersResult {
tx_hash: TxHash::default(),
orders: Vec::new(),
timestamp: chrono::Utc::now().timestamp(),
});
}
let currency0_price = PoolViewer::currency0_price(self, None).await?;
let current_tick = self.price_to_closest_tick(currency0_price.clone())?.as_i32();
let token_ids: Vec<U256> =
params.items.iter().map(|item| item.order_metadata.token_id).collect();
let positions = PoolMaker::get_positions(self, &token_ids, None).await?;
let mut batch_items = Vec::with_capacity(params.items.len());
for (item, position) in params.items.iter().zip(positions.iter()) {
if !self.can_fulfill_order(
current_tick,
position.tick_lower,
position.tick_upper,
item.order_metadata.order_side,
)? {
return Err(anyhow::anyhow!(
"Order {} cannot be fulfilled yet. Current tick {} has not crossed through \
position range [{}, {}) for {:?} order.",
item.order_metadata.token_id,
current_tick,
position.tick_lower,
position.tick_upper,
item.order_metadata.order_side
));
}
let tick_lower_i24 = I24::try_from(position.tick_lower)?;
let tick_upper_i24 = I24::try_from(position.tick_upper)?;
batch_items.push(RemoveBatchLiquidityItem {
token_id: item.order_metadata.token_id,
liquidity: U256::from(position.liquidity),
tick_lower: tick_lower_i24,
tick_upper: tick_upper_i24,
burn_token: true,
});
}
let batch_params = RemoveBatchLiquidityParams {
recipient: params.recipient,
deadline: params.deadline,
token0_price: currency0_price.clone(),
items: batch_items,
};
let batch_options = RemoveLiquidityOptions {
slippage_tolerance: options.slippage_tolerance,
burn_token: true,
permit: None,
collect_options: Default::default(),
};
let result = PoolInfuser::remove_batch_liquidity(self, batch_params, batch_options).await?;
let timestamp = chrono::Utc::now().timestamp();
let orders: Vec<FulfillOrderResultItem> = result
.results
.into_iter()
.zip(params.items.iter())
.zip(positions.iter())
.map(|((remove_result, item), position)| {
let (base_amount, quote_amount) = if self.is_token_a_base {
(remove_result.amount0, remove_result.amount1)
} else {
(remove_result.amount1, remove_result.amount0)
};
let tick_lower_i24 = I24::try_from(position.tick_lower).unwrap_or_default();
let fill_price =
self.tick_to_base_price(tick_lower_i24).unwrap_or(currency0_price.clone());
FulfillOrderResultItem {
token_id: item.order_metadata.token_id,
fully_filled: true,
base_amount,
quote_amount,
fill_price,
fee: None,
tick_lower: position.tick_lower,
tick_upper: position.tick_upper,
}
})
.collect();
Ok(FulfillBatchOrdersResult { tx_hash: result.tx_hash, orders, timestamp })
}
#[instrument(skip(self), fields(token_id = ?order_metadata.token_id, order_side = ?order_metadata.order_side))]
async fn get_order(&self, order_metadata: OrderMetadata) -> Result<Option<OrderInfo>> {
debug!(token_id = ?order_metadata.token_id, order_side = ?order_metadata.order_side, "get_order not yet fully implemented");
Ok(None)
}
#[instrument(skip(self), fields(token_id = ?token_id))]
async fn get_position(
&self,
token_id: U256,
block_id: Option<BlockId>,
) -> Result<PositionInfo> {
let position_manager = uniswap_lens::bindings::iuniswapv3nonfungiblepositionmanager::IUniswapV3NonfungiblePositionManager::IUniswapV3NonfungiblePositionManagerInstance::new(
self.position_manager_address(),
self.provider.clone(),
);
let position_data = position_manager.positions(token_id).call().await?;
let sqrt_price_x96 = PoolState::sqrt_price_x96(self, block_id).await?;
self.calculate_position_info(
token_id,
position_data.tickLower.as_i32(),
position_data.tickUpper.as_i32(),
position_data.liquidity,
position_data.tokensOwed0,
position_data.tokensOwed1,
sqrt_price_x96,
)
}
#[instrument(skip(self, token_ids), fields(num_positions = token_ids.len()))]
async fn get_positions(
&self,
token_ids: &[U256],
block_id: Option<BlockId>,
) -> Result<Vec<PositionInfo>> {
if token_ids.is_empty() {
return Ok(Vec::new());
}
let position_manager = uniswap_lens::bindings::iuniswapv3nonfungiblepositionmanager::IUniswapV3NonfungiblePositionManager::IUniswapV3NonfungiblePositionManagerInstance::new(
self.position_manager_address(),
self.provider.clone(),
);
let mut multicall = alloy::providers::Provider::multicall(&self.provider).dynamic();
for token_id in token_ids {
multicall = multicall.add_dynamic(position_manager.positions(*token_id));
}
let block_id = block_id.unwrap_or(BlockId::latest());
let position_data = multicall.block(block_id).aggregate().await?;
let sqrt_price_x96 = PoolState::sqrt_price_x96(self, Some(block_id)).await?;
let mut results = Vec::with_capacity(token_ids.len());
for (token_id, pos_data) in token_ids.iter().zip(position_data.iter()) {
let position_info = self.calculate_position_info(
*token_id,
pos_data.tickLower.as_i32(),
pos_data.tickUpper.as_i32(),
pos_data.liquidity,
pos_data.tokensOwed0,
pos_data.tokensOwed1,
sqrt_price_x96,
)?;
results.push(position_info);
}
debug!(num_positions = results.len(), "Fetched positions via multicall");
Ok(results)
}
#[instrument(skip(self, orders), fields(num_orders = orders.len()))]
async fn get_orders(
&self,
orders: &[OrderMetadata],
_block_id: Option<BlockId>,
) -> Result<Vec<OrderInfo>> {
debug!(num_orders = orders.len(), "get_orders not yet fully implemented");
Ok(Vec::new())
}
#[instrument(skip(self, params), fields(token_id = ?params.token_id))]
async fn cancel_order(
&self,
params: crate::traits::pool_maker::CancelOrderParams,
) -> Result<CancelOrderResult> {
let currency0_price = PoolViewer::currency0_price(self, None).await?;
let position = PoolMaker::get_position(self, params.token_id, None).await?;
let tick_lower_i24 = I24::try_from(position.tick_lower)?;
let tick_upper_i24 = I24::try_from(position.tick_upper)?;
let remove_params = RemoveLiquidityParams {
token_id: params.token_id,
liquidity: U256::from(position.liquidity),
recipient: params.recipient,
deadline: params.deadline,
token0_price: currency0_price,
tick_lower: tick_lower_i24,
tick_upper: tick_upper_i24,
};
let remove_options = RemoveLiquidityOptions {
slippage_tolerance: params.slippage_tolerance,
burn_token: true,
permit: None,
collect_options: Default::default(),
};
let result = PoolInfuser::remove_liquidity(self, remove_params, remove_options).await?;
let timestamp = chrono::Utc::now().timestamp();
info!(token_id = ?params.token_id, "Order cancelled successfully");
Ok(CancelOrderResult {
token_id: params.token_id,
tx_hash: result.tx_hash,
tick_lower: position.tick_lower,
tick_upper: position.tick_upper,
timestamp,
})
}
#[instrument(skip(self, params), fields(num_orders = params.token_ids.len()))]
async fn cancel_batch_orders(
&self,
params: crate::traits::pool_maker::CancelBatchOrdersParams,
) -> Result<CancelBatchOrdersResult> {
use crate::traits::pool_infuser::{
RemoveBatchLiquidityItem, RemoveBatchLiquidityParams, RemoveLiquidityOptions,
};
if params.token_ids.is_empty() {
return Ok(CancelBatchOrdersResult {
tx_hash: TxHash::default(),
cancelled_orders: Vec::new(),
timestamp: chrono::Utc::now().timestamp(),
});
}
let currency0_price = PoolViewer::currency0_price(self, None).await?;
let positions = PoolMaker::get_positions(self, ¶ms.token_ids, None).await?;
let mut batch_items = Vec::with_capacity(params.token_ids.len());
for (token_id, position) in params.token_ids.iter().zip(positions.iter()) {
let tick_lower_i24 = I24::try_from(position.tick_lower)?;
let tick_upper_i24 = I24::try_from(position.tick_upper)?;
batch_items.push(RemoveBatchLiquidityItem {
token_id: *token_id,
liquidity: U256::from(position.liquidity),
tick_lower: tick_lower_i24,
tick_upper: tick_upper_i24,
burn_token: true,
});
}
let batch_params = RemoveBatchLiquidityParams {
recipient: params.recipient,
deadline: params.deadline,
token0_price: currency0_price,
items: batch_items,
};
let batch_options = RemoveLiquidityOptions {
slippage_tolerance: params.slippage_tolerance,
burn_token: true,
permit: None,
collect_options: Default::default(),
};
let result = PoolInfuser::remove_batch_liquidity(self, batch_params, batch_options).await?;
let timestamp = chrono::Utc::now().timestamp();
let cancelled_orders: Vec<CancelOrderResultItem> = params
.token_ids
.iter()
.zip(positions.iter())
.map(|(token_id, position)| CancelOrderResultItem {
token_id: *token_id,
tick_lower: position.tick_lower,
tick_upper: position.tick_upper,
})
.collect();
info!(num_cancelled = params.token_ids.len(), "Batch orders cancelled successfully");
Ok(CancelBatchOrdersResult { tx_hash: result.tx_hash, cancelled_orders, timestamp })
}
#[instrument(skip(self), fields(token_id = ?order_metadata.token_id, order_side = ?order_metadata.order_side))]
async fn can_fulfill(
&self,
order_metadata: OrderMetadata,
block_id: Option<BlockId>,
) -> Result<bool> {
let currency0_price = PoolViewer::currency0_price(self, block_id).await?;
let position = PoolMaker::get_position(self, order_metadata.token_id, block_id).await?;
let current_tick = self.price_to_closest_tick(currency0_price.clone())?.as_i32();
let can_fulfill = self.can_fulfill_order(
current_tick,
position.tick_lower,
position.tick_upper,
order_metadata.order_side,
)?;
debug!(
token_id = ?order_metadata.token_id,
current_tick = current_tick,
position_tick_lower = position.tick_lower,
position_tick_upper = position.tick_upper,
order_side = ?order_metadata.order_side,
can_fulfill = can_fulfill,
"can_fulfill check"
);
Ok(can_fulfill)
}
#[instrument(skip(self, current_price), fields(price = ?price_input.price, side = ?price_input.side))]
fn check_price_fulfillable(
&self,
current_price: &Price<Currency, Currency>,
price_input: PriceCheckInput,
) -> Result<PriceCheckResult> {
let prices = vec![price_input];
let results = PoolMaker::check_prices_fulfillable(self, current_price, prices)?;
Ok(results[0].clone())
}
#[instrument(skip(self, current_price), fields(count = prices.len()))]
fn check_prices_fulfillable(
&self,
current_price: &Price<Currency, Currency>,
prices: Vec<PriceCheckInput>,
) -> Result<Vec<PriceCheckResult>> {
if prices.is_empty() {
return Ok(vec![]);
}
let current_tick = self.base_price_to_closest_tick(current_price)?.as_i32();
let restricted_range = PoolMaker::get_restricted_price_range(self, current_tick)?;
let mut results = Vec::with_capacity(prices.len());
for price_input in prices {
let order_tick = self.base_price_to_closest_tick(&price_input.price)?.as_i32();
let (tick_lower, tick_upper) =
self.get_order_tick_range(order_tick, price_input.side)?;
let tick_lower_i32 = tick_lower.as_i32();
let tick_upper_i32 = tick_upper.as_i32();
let is_fulfillable = self.can_fulfill_order_range(
&restricted_range,
tick_lower_i32,
tick_upper_i32,
price_input.side,
);
results.push(PriceCheckResult {
price: price_input.price,
side: price_input.side,
is_fulfillable,
});
}
debug!(
checked_count = results.len(),
fulfillable_count = results.iter().filter(|r| r.is_fulfillable).count(),
"check_prices_fulfillable completed"
);
Ok(results)
}
#[instrument(skip(self, current_price), fields(price = ?price_input.price, side = ?price_input.side))]
fn check_price_placeable(
&self,
current_price: &Price<Currency, Currency>,
price_input: PriceCheckInput,
) -> Result<PricePlaceableResult> {
let prices = vec![price_input];
let results = PoolMaker::check_prices_placeable(self, current_price, prices)?;
Ok(results[0].clone())
}
#[instrument(skip(self, current_price), fields(count = prices.len()))]
fn check_prices_placeable(
&self,
current_price: &Price<Currency, Currency>,
prices: Vec<PriceCheckInput>,
) -> Result<Vec<PricePlaceableResult>> {
if prices.is_empty() {
return Ok(vec![]);
}
let current_tick = self.base_price_to_closest_tick(current_price)?.as_i32();
let restricted_range = PoolMaker::get_restricted_price_range(self, current_tick)?;
let mut results = Vec::with_capacity(prices.len());
for price_input in prices {
let order_tick = self.base_price_to_closest_tick(&price_input.price)?.as_i32();
let (tick_lower, tick_upper) =
self.get_order_tick_range(order_tick, price_input.side)?;
let tick_lower_i32 = tick_lower.as_i32();
let tick_upper_i32 = tick_upper.as_i32();
let is_placeable = self.can_place_order_range(
&restricted_range,
tick_lower_i32,
tick_upper_i32,
price_input.side,
);
results.push(PricePlaceableResult {
price: price_input.price,
side: price_input.side,
is_placeable,
});
}
debug!(
checked_count = results.len(),
placeable_count = results.iter().filter(|r| r.is_placeable).count(),
"check_prices_placeable completed"
);
Ok(results)
}
#[instrument(skip(self))]
async fn get_market_price(
&self,
block_id: Option<BlockId>,
) -> Result<Price<Currency, Currency>> {
if self.is_token_a_base {
PoolViewer::currency0_price(self, block_id).await
} else {
PoolViewer::currency1_price(self, block_id).await
}
}
#[instrument(skip(self, price), fields(price = ?price, side = ?side))]
fn get_order_price_range(
&self,
price: &Price<Currency, Currency>,
side: OrderSide,
) -> Result<OrderPriceRange> {
let order_tick = self.base_price_to_closest_tick(price)?.as_i32();
let (tick_lower, tick_upper) = self.get_order_tick_range(order_tick, side)?;
let tick_lower_i32 = tick_lower.as_i32();
let tick_upper_i32 = tick_upper.as_i32();
let lower_price = self.tick_to_base_price(tick_lower)?;
let upper_price = self.tick_to_base_price(tick_upper)?;
Ok(OrderPriceRange {
lower_price,
upper_price,
tick_lower: tick_lower_i32,
tick_upper: tick_upper_i32,
side,
})
}
#[instrument(skip(self))]
fn get_restricted_price_range(&self, current_tick: i32) -> Result<RestrictedPriceRange> {
let tick_lower = (current_tick / self.tick_spacing) * self.tick_spacing - self.tick_spacing;
let tick_upper = tick_lower + 2 * self.tick_spacing;
let tick_lower_i24 = I24::try_from(tick_lower)?;
let tick_upper_i24 = I24::try_from(tick_upper)?;
let lower_price = self.tick_to_base_price(tick_lower_i24)?;
let upper_price = self.tick_to_base_price(tick_upper_i24)?;
Ok(RestrictedPriceRange { lower_price, upper_price, tick_lower, tick_upper })
}
#[instrument(skip(self, _side, _amount), fields(side = ?_side, amount = ?_amount))]
async fn estimate_execution_price(
&self,
_side: OrderSide,
_amount: CurrencyAmount<Currency>,
block_id: Option<BlockId>,
) -> Result<Price<Currency, Currency>> {
self.get_market_price(block_id).await
}
}