1use std::{collections::HashMap, sync::Arc};
17
18use alloy::{
19 primitives::{Address, U160, U256, keccak256},
20 sol,
21 sol_types::{SolCall, private::primitives::aliases::I24},
22};
23use nautilus_model::{
24 defi::{
25 data::block::BlockPosition,
26 pool_analysis::{
27 position::PoolPosition,
28 snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
29 },
30 tick_map::tick::PoolTick,
31 },
32 identifiers::InstrumentId,
33};
34use thiserror::Error;
35
36use super::base::{BaseContract, ContractCall};
37use crate::rpc::{error::BlockchainRpcClientError, http::BlockchainHttpRpcClient};
38
39sol! {
40 #[sol(rpc)]
41 contract UniswapV3Pool {
42 struct Slot0Data {
44 uint160 sqrtPriceX96;
45 int24 tick;
46 uint16 observationIndex;
47 uint16 observationCardinality;
48 uint16 observationCardinalityNext;
49 uint8 feeProtocol;
50 bool unlocked;
51 }
52
53 struct TickInfo {
55 uint128 liquidityGross;
56 int128 liquidityNet;
57 uint256 feeGrowthOutside0X128;
58 uint256 feeGrowthOutside1X128;
59 int56 tickCumulativeOutside;
60 uint160 secondsPerLiquidityOutsideX128;
61 uint32 secondsOutside;
62 bool initialized;
63 }
64
65 struct PositionInfo {
67 uint128 liquidity;
68 uint256 feeGrowthInside0LastX128;
69 uint256 feeGrowthInside1LastX128;
70 uint128 tokensOwed0;
71 uint128 tokensOwed1;
72 }
73
74 function slot0() external view returns (Slot0Data memory);
76 function liquidity() external view returns (uint128);
77 function feeGrowthGlobal0X128() external view returns (uint256);
78 function feeGrowthGlobal1X128() external view returns (uint256);
79
80 function ticks(int24 tick) external view returns (TickInfo memory);
82 function positions(bytes32 key) external view returns (PositionInfo memory);
83 }
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct PoolGlobalState {
89 pub sqrt_price_x96: U160,
91 pub tick: i32,
93 pub liquidity: u128,
95 pub fee_growth_global_0_x128: U256,
97 pub fee_growth_global_1_x128: U256,
99 pub fee_protocol: u8,
101}
102
103#[derive(Debug, Error)]
105pub enum UniswapV3PoolError {
106 #[error("RPC error: {0}")]
107 RpcError(#[from] BlockchainRpcClientError),
108 #[error("Failed to decode {field} for pool {pool}: {reason} (raw data: {raw_data})")]
109 DecodingError {
110 field: String,
111 pool: Address,
112 reason: String,
113 raw_data: String,
114 },
115 #[error("Call failed for {field} at pool {pool}: {reason}")]
116 CallFailed {
117 field: String,
118 pool: Address,
119 reason: String,
120 },
121 #[error("Tick {tick} is not initialized in pool {pool}")]
122 TickNotInitialized { tick: i32, pool: Address },
123}
124
125#[derive(Debug)]
131pub struct UniswapV3PoolContract {
132 base: BaseContract,
134}
135
136impl UniswapV3PoolContract {
137 #[must_use]
139 pub fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
140 Self {
141 base: BaseContract::new(client),
142 }
143 }
144
145 pub async fn get_global_state(
151 &self,
152 pool_address: &Address,
153 block: Option<u64>,
154 ) -> Result<PoolGlobalState, UniswapV3PoolError> {
155 let calls = vec![
156 ContractCall {
157 target: *pool_address,
158 allow_failure: false,
159 call_data: UniswapV3Pool::slot0Call {}.abi_encode(),
160 },
161 ContractCall {
162 target: *pool_address,
163 allow_failure: false,
164 call_data: UniswapV3Pool::liquidityCall {}.abi_encode(),
165 },
166 ContractCall {
167 target: *pool_address,
168 allow_failure: false,
169 call_data: UniswapV3Pool::feeGrowthGlobal0X128Call {}.abi_encode(),
170 },
171 ContractCall {
172 target: *pool_address,
173 allow_failure: false,
174 call_data: UniswapV3Pool::feeGrowthGlobal1X128Call {}.abi_encode(),
175 },
176 ];
177
178 let results = self.base.execute_multicall(calls, block).await?;
179
180 if results.len() != 4 {
181 return Err(UniswapV3PoolError::CallFailed {
182 field: "global_state_multicall".to_string(),
183 pool: *pool_address,
184 reason: format!("Expected 4 results, got {}", results.len()),
185 });
186 }
187
188 let slot0 =
190 UniswapV3Pool::slot0Call::abi_decode_returns(&results[0].returnData).map_err(|e| {
191 UniswapV3PoolError::DecodingError {
192 field: "slot0".to_string(),
193 pool: *pool_address,
194 reason: e.to_string(),
195 raw_data: hex::encode(&results[0].returnData),
196 }
197 })?;
198
199 let liquidity = UniswapV3Pool::liquidityCall::abi_decode_returns(&results[1].returnData)
201 .map_err(|e| UniswapV3PoolError::DecodingError {
202 field: "liquidity".to_string(),
203 pool: *pool_address,
204 reason: e.to_string(),
205 raw_data: hex::encode(&results[1].returnData),
206 })?;
207
208 let fee_growth_0 =
210 UniswapV3Pool::feeGrowthGlobal0X128Call::abi_decode_returns(&results[2].returnData)
211 .map_err(|e| UniswapV3PoolError::DecodingError {
212 field: "feeGrowthGlobal0X128".to_string(),
213 pool: *pool_address,
214 reason: e.to_string(),
215 raw_data: hex::encode(&results[2].returnData),
216 })?;
217
218 let fee_growth_1 =
220 UniswapV3Pool::feeGrowthGlobal1X128Call::abi_decode_returns(&results[3].returnData)
221 .map_err(|e| UniswapV3PoolError::DecodingError {
222 field: "feeGrowthGlobal1X128".to_string(),
223 pool: *pool_address,
224 reason: e.to_string(),
225 raw_data: hex::encode(&results[3].returnData),
226 })?;
227
228 Ok(PoolGlobalState {
229 sqrt_price_x96: slot0.sqrtPriceX96,
230 tick: slot0.tick.as_i32(),
231 liquidity,
232 fee_growth_global_0_x128: fee_growth_0,
233 fee_growth_global_1_x128: fee_growth_1,
234 fee_protocol: slot0.feeProtocol,
235 })
236 }
237
238 pub async fn get_tick(
244 &self,
245 pool_address: &Address,
246 tick: i32,
247 block: Option<u64>,
248 ) -> Result<PoolTick, UniswapV3PoolError> {
249 let tick_i24 = I24::try_from(tick).map_err(|_| UniswapV3PoolError::CallFailed {
250 field: "tick".to_string(),
251 pool: *pool_address,
252 reason: format!("Tick {} out of range for int24", tick),
253 })?;
254
255 let call_data = UniswapV3Pool::ticksCall { tick: tick_i24 }.abi_encode();
256 let raw_response = self
257 .base
258 .execute_call(pool_address, &call_data, block)
259 .await?;
260
261 let tick_info =
262 UniswapV3Pool::ticksCall::abi_decode_returns(&raw_response).map_err(|e| {
263 UniswapV3PoolError::DecodingError {
264 field: format!("ticks({})", tick),
265 pool: *pool_address,
266 reason: e.to_string(),
267 raw_data: hex::encode(&raw_response),
268 }
269 })?;
270
271 Ok(PoolTick::new(
272 tick,
273 tick_info.liquidityGross,
274 tick_info.liquidityNet,
275 tick_info.feeGrowthOutside0X128,
276 tick_info.feeGrowthOutside1X128,
277 tick_info.initialized,
278 0, ))
280 }
281
282 pub async fn batch_get_ticks(
289 &self,
290 pool_address: &Address,
291 ticks: &[i32],
292 block: Option<u64>,
293 ) -> Result<HashMap<i32, PoolTick>, UniswapV3PoolError> {
294 let calls: Vec<ContractCall> = ticks
295 .iter()
296 .filter_map(|&tick| {
297 I24::try_from(tick).ok().map(|tick_i24| ContractCall {
298 target: *pool_address,
299 allow_failure: true,
300 call_data: UniswapV3Pool::ticksCall { tick: tick_i24 }.abi_encode(),
301 })
302 })
303 .collect();
304
305 let results = self.base.execute_multicall(calls, block).await?;
306
307 let mut tick_infos = HashMap::with_capacity(ticks.len());
308 for (i, &tick_value) in ticks.iter().enumerate() {
309 if i >= results.len() {
310 break;
311 }
312
313 let result = &results[i];
314 if !result.success {
315 continue;
317 }
318
319 let tick_info = UniswapV3Pool::ticksCall::abi_decode_returns(&result.returnData)
320 .map_err(|e| UniswapV3PoolError::DecodingError {
321 field: format!("ticks({})", tick_value),
322 pool: *pool_address,
323 reason: e.to_string(),
324 raw_data: hex::encode(&result.returnData),
325 })?;
326
327 tick_infos.insert(
328 tick_value,
329 PoolTick::new(
330 tick_value,
331 tick_info.liquidityGross,
332 tick_info.liquidityNet,
333 tick_info.feeGrowthOutside0X128,
334 tick_info.feeGrowthOutside1X128,
335 tick_info.initialized,
336 0, ),
338 );
339 }
340
341 Ok(tick_infos)
342 }
343
344 #[must_use]
348 pub fn compute_position_key(owner: &Address, tick_lower: i32, tick_upper: i32) -> [u8; 32] {
349 let mut packed = Vec::with_capacity(26);
351
352 packed.extend_from_slice(owner.as_slice());
354
355 let tick_lower_bytes = tick_lower.to_be_bytes();
357 packed.extend_from_slice(&tick_lower_bytes[1..4]);
358
359 let tick_upper_bytes = tick_upper.to_be_bytes();
361 packed.extend_from_slice(&tick_upper_bytes[1..4]);
362
363 keccak256(&packed).into()
364 }
365
366 pub async fn batch_get_positions(
373 &self,
374 pool_address: &Address,
375 positions: &[(Address, i32, i32)],
376 block: Option<u64>,
377 ) -> Result<Vec<PoolPosition>, UniswapV3PoolError> {
378 let calls: Vec<ContractCall> = positions
379 .iter()
380 .map(|(owner, tick_lower, tick_upper)| {
381 let position_key = Self::compute_position_key(owner, *tick_lower, *tick_upper);
382 ContractCall {
383 target: *pool_address,
384 allow_failure: true,
385 call_data: UniswapV3Pool::positionsCall {
386 key: position_key.into(),
387 }
388 .abi_encode(),
389 }
390 })
391 .collect();
392
393 let results = self.base.execute_multicall(calls, block).await?;
394
395 let position_infos: Vec<PoolPosition> = positions
396 .iter()
397 .enumerate()
398 .filter_map(|(i, (owner, tick_lower, tick_upper))| {
399 if i >= results.len() {
400 return None;
401 }
402
403 let result = &results[i];
404 if !result.success {
405 return None;
406 }
407
408 UniswapV3Pool::positionsCall::abi_decode_returns(&result.returnData)
409 .ok()
410 .map(|info| PoolPosition {
411 owner: *owner,
412 tick_lower: *tick_lower,
413 tick_upper: *tick_upper,
414 liquidity: info.liquidity,
415 fee_growth_inside_0_last: info.feeGrowthInside0LastX128,
416 fee_growth_inside_1_last: info.feeGrowthInside1LastX128,
417 tokens_owed_0: info.tokensOwed0,
418 tokens_owed_1: info.tokensOwed1,
419 total_amount0_deposited: U256::ZERO,
420 total_amount1_deposited: U256::ZERO,
421 total_amount0_collected: 0,
422 total_amount1_collected: 0,
423 })
424 })
425 .collect();
426
427 Ok(position_infos)
428 }
429
430 pub async fn fetch_snapshot(
440 &self,
441 pool_address: &Address,
442 instrument_id: InstrumentId,
443 tick_values: &[i32],
444 position_keys: &[(Address, i32, i32)],
445 block: Option<u64>,
446 ) -> Result<PoolSnapshot, UniswapV3PoolError> {
447 let global_state = self.get_global_state(pool_address, block).await?;
449 let ticks_map = self
450 .batch_get_ticks(pool_address, tick_values, block)
451 .await?;
452 let positions = self
453 .batch_get_positions(pool_address, position_keys, block)
454 .await?;
455
456 let ticks: Vec<PoolTick> = ticks_map.into_values().collect();
458
459 let pool_state = PoolState {
461 current_tick: global_state.tick,
462 price_sqrt_ratio_x96: global_state.sqrt_price_x96,
463 liquidity: global_state.liquidity,
464 protocol_fees_token0: U256::ZERO,
465 protocol_fees_token1: U256::ZERO,
466 fee_protocol: global_state.fee_protocol,
467 fee_growth_global_0: global_state.fee_growth_global_0_x128,
468 fee_growth_global_1: global_state.fee_growth_global_1_x128,
469 };
470
471 let analytics = PoolAnalytics::default();
473 let block_position = BlockPosition {
474 number: 0,
475 transaction_hash: String::new(),
476 transaction_index: 0,
477 log_index: 0,
478 };
479
480 Ok(PoolSnapshot::new(
481 instrument_id,
482 pool_state,
483 positions,
484 ticks,
485 analytics,
486 block_position,
487 ))
488 }
489}