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_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, Error)]
88pub enum UniswapV3PoolError {
89 #[error("RPC error: {0}")]
90 RpcError(#[from] BlockchainRpcClientError),
91 #[error("Failed to decode {field} for pool {pool}: {reason} (raw data: {raw_data})")]
92 DecodingError {
93 field: String,
94 pool: Address,
95 reason: String,
96 raw_data: String,
97 },
98 #[error("Call failed for {field} at pool {pool}: {reason}")]
99 CallFailed {
100 field: String,
101 pool: Address,
102 reason: String,
103 },
104 #[error("Tick {tick} is not initialized in pool {pool}")]
105 TickNotInitialized { tick: i32, pool: Address },
106}
107
108#[derive(Debug)]
114pub struct UniswapV3PoolContract {
115 base: BaseContract,
117}
118
119impl UniswapV3PoolContract {
120 #[must_use]
122 pub fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
123 Self {
124 base: BaseContract::new(client),
125 }
126 }
127
128 pub async fn get_global_state(
134 &self,
135 pool_address: &Address,
136 block: Option<u64>,
137 ) -> Result<PoolState, UniswapV3PoolError> {
138 let calls = vec![
139 ContractCall {
140 target: *pool_address,
141 allow_failure: false,
142 call_data: UniswapV3Pool::slot0Call {}.abi_encode(),
143 },
144 ContractCall {
145 target: *pool_address,
146 allow_failure: false,
147 call_data: UniswapV3Pool::liquidityCall {}.abi_encode(),
148 },
149 ContractCall {
150 target: *pool_address,
151 allow_failure: false,
152 call_data: UniswapV3Pool::feeGrowthGlobal0X128Call {}.abi_encode(),
153 },
154 ContractCall {
155 target: *pool_address,
156 allow_failure: false,
157 call_data: UniswapV3Pool::feeGrowthGlobal1X128Call {}.abi_encode(),
158 },
159 ];
160
161 let results = self.base.execute_multicall(calls, block).await?;
162
163 if results.len() != 4 {
164 return Err(UniswapV3PoolError::CallFailed {
165 field: "global_state_multicall".to_string(),
166 pool: *pool_address,
167 reason: format!("Expected 4 results, got {}", results.len()),
168 });
169 }
170
171 let slot0 =
173 UniswapV3Pool::slot0Call::abi_decode_returns(&results[0].returnData).map_err(|e| {
174 UniswapV3PoolError::DecodingError {
175 field: "slot0".to_string(),
176 pool: *pool_address,
177 reason: e.to_string(),
178 raw_data: hex::encode(&results[0].returnData),
179 }
180 })?;
181
182 let liquidity = UniswapV3Pool::liquidityCall::abi_decode_returns(&results[1].returnData)
184 .map_err(|e| UniswapV3PoolError::DecodingError {
185 field: "liquidity".to_string(),
186 pool: *pool_address,
187 reason: e.to_string(),
188 raw_data: hex::encode(&results[1].returnData),
189 })?;
190
191 let fee_growth_0 =
193 UniswapV3Pool::feeGrowthGlobal0X128Call::abi_decode_returns(&results[2].returnData)
194 .map_err(|e| UniswapV3PoolError::DecodingError {
195 field: "feeGrowthGlobal0X128".to_string(),
196 pool: *pool_address,
197 reason: e.to_string(),
198 raw_data: hex::encode(&results[2].returnData),
199 })?;
200
201 let fee_growth_1 =
203 UniswapV3Pool::feeGrowthGlobal1X128Call::abi_decode_returns(&results[3].returnData)
204 .map_err(|e| UniswapV3PoolError::DecodingError {
205 field: "feeGrowthGlobal1X128".to_string(),
206 pool: *pool_address,
207 reason: e.to_string(),
208 raw_data: hex::encode(&results[3].returnData),
209 })?;
210
211 Ok(PoolState {
212 current_tick: slot0.tick.as_i32(),
213 price_sqrt_ratio_x96: slot0.sqrtPriceX96,
214 liquidity,
215 protocol_fees_token0: U256::ZERO,
216 protocol_fees_token1: U256::ZERO,
217 fee_protocol: slot0.feeProtocol,
218 fee_growth_global_0: fee_growth_0,
219 fee_growth_global_1: fee_growth_1,
220 })
221 }
222
223 pub async fn get_tick(
229 &self,
230 pool_address: &Address,
231 tick: i32,
232 block: Option<u64>,
233 ) -> Result<PoolTick, UniswapV3PoolError> {
234 let tick_i24 = I24::try_from(tick).map_err(|_| UniswapV3PoolError::CallFailed {
235 field: "tick".to_string(),
236 pool: *pool_address,
237 reason: format!("Tick {} out of range for int24", tick),
238 })?;
239
240 let call_data = UniswapV3Pool::ticksCall { tick: tick_i24 }.abi_encode();
241 let raw_response = self
242 .base
243 .execute_call(pool_address, &call_data, block)
244 .await?;
245
246 let tick_info =
247 UniswapV3Pool::ticksCall::abi_decode_returns(&raw_response).map_err(|e| {
248 UniswapV3PoolError::DecodingError {
249 field: format!("ticks({})", tick),
250 pool: *pool_address,
251 reason: e.to_string(),
252 raw_data: hex::encode(&raw_response),
253 }
254 })?;
255
256 Ok(PoolTick::new(
257 tick,
258 tick_info.liquidityGross,
259 tick_info.liquidityNet,
260 tick_info.feeGrowthOutside0X128,
261 tick_info.feeGrowthOutside1X128,
262 tick_info.initialized,
263 0, ))
265 }
266
267 pub async fn batch_get_ticks(
274 &self,
275 pool_address: &Address,
276 ticks: &[i32],
277 block: Option<u64>,
278 ) -> Result<HashMap<i32, PoolTick>, UniswapV3PoolError> {
279 let calls: Vec<ContractCall> = ticks
280 .iter()
281 .filter_map(|&tick| {
282 I24::try_from(tick).ok().map(|tick_i24| ContractCall {
283 target: *pool_address,
284 allow_failure: true,
285 call_data: UniswapV3Pool::ticksCall { tick: tick_i24 }.abi_encode(),
286 })
287 })
288 .collect();
289
290 let results = self.base.execute_multicall(calls, block).await?;
291
292 let mut tick_infos = HashMap::with_capacity(ticks.len());
293 for (i, &tick_value) in ticks.iter().enumerate() {
294 if i >= results.len() {
295 break;
296 }
297
298 let result = &results[i];
299 if !result.success {
300 continue;
302 }
303
304 let tick_info = UniswapV3Pool::ticksCall::abi_decode_returns(&result.returnData)
305 .map_err(|e| UniswapV3PoolError::DecodingError {
306 field: format!("ticks({})", tick_value),
307 pool: *pool_address,
308 reason: e.to_string(),
309 raw_data: hex::encode(&result.returnData),
310 })?;
311
312 tick_infos.insert(
313 tick_value,
314 PoolTick::new(
315 tick_value,
316 tick_info.liquidityGross,
317 tick_info.liquidityNet,
318 tick_info.feeGrowthOutside0X128,
319 tick_info.feeGrowthOutside1X128,
320 tick_info.initialized,
321 0, ),
323 );
324 }
325
326 Ok(tick_infos)
327 }
328
329 #[must_use]
333 pub fn compute_position_key(owner: &Address, tick_lower: i32, tick_upper: i32) -> [u8; 32] {
334 let mut packed = Vec::with_capacity(26);
336
337 packed.extend_from_slice(owner.as_slice());
339
340 let tick_lower_bytes = tick_lower.to_be_bytes();
342 packed.extend_from_slice(&tick_lower_bytes[1..4]);
343
344 let tick_upper_bytes = tick_upper.to_be_bytes();
346 packed.extend_from_slice(&tick_upper_bytes[1..4]);
347
348 keccak256(&packed).into()
349 }
350
351 pub async fn batch_get_positions(
358 &self,
359 pool_address: &Address,
360 positions: &[(Address, i32, i32)],
361 block: Option<u64>,
362 ) -> Result<Vec<PoolPosition>, UniswapV3PoolError> {
363 let calls: Vec<ContractCall> = positions
364 .iter()
365 .map(|(owner, tick_lower, tick_upper)| {
366 let position_key = Self::compute_position_key(owner, *tick_lower, *tick_upper);
367 ContractCall {
368 target: *pool_address,
369 allow_failure: true,
370 call_data: UniswapV3Pool::positionsCall {
371 key: position_key.into(),
372 }
373 .abi_encode(),
374 }
375 })
376 .collect();
377
378 let results = self.base.execute_multicall(calls, block).await?;
379
380 let position_infos: Vec<PoolPosition> = positions
381 .iter()
382 .enumerate()
383 .filter_map(|(i, (owner, tick_lower, tick_upper))| {
384 if i >= results.len() {
385 return None;
386 }
387
388 let result = &results[i];
389 if !result.success {
390 return None;
391 }
392
393 UniswapV3Pool::positionsCall::abi_decode_returns(&result.returnData)
394 .ok()
395 .map(|info| PoolPosition {
396 owner: *owner,
397 tick_lower: *tick_lower,
398 tick_upper: *tick_upper,
399 liquidity: info.liquidity,
400 fee_growth_inside_0_last: info.feeGrowthInside0LastX128,
401 fee_growth_inside_1_last: info.feeGrowthInside1LastX128,
402 tokens_owed_0: info.tokensOwed0,
403 tokens_owed_1: info.tokensOwed1,
404 total_amount0_deposited: U256::ZERO,
405 total_amount1_deposited: U256::ZERO,
406 total_amount0_collected: 0,
407 total_amount1_collected: 0,
408 })
409 })
410 .collect();
411
412 Ok(position_infos)
413 }
414
415 pub async fn fetch_snapshot(
425 &self,
426 pool_address: &Address,
427 instrument_id: InstrumentId,
428 tick_values: &[i32],
429 position_keys: &[(Address, i32, i32)],
430 block_position: BlockPosition,
431 ) -> Result<PoolSnapshot, UniswapV3PoolError> {
432 let block = Some(block_position.number);
434 let global_state = self.get_global_state(pool_address, block).await?;
435 let ticks_map = self
436 .batch_get_ticks(pool_address, tick_values, block)
437 .await?;
438 let positions = self
439 .batch_get_positions(pool_address, position_keys, block)
440 .await?;
441
442 Ok(PoolSnapshot::new(
443 instrument_id,
444 global_state,
445 positions,
446 ticks_map.into_values().collect(),
447 PoolAnalytics::default(),
448 block_position,
449 ))
450 }
451}