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