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