nautilus_blockchain/exchanges/parsing/uniswap_v4/
initialize.rs1use alloy::{dyn_abi::SolType, primitives::Address, sol};
17use nautilus_model::defi::{PoolIdentifier, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21 events::pool_created::PoolCreatedEvent,
22 hypersync::{
23 HypersyncLog,
24 helpers::{extract_block_number, validate_event_signature_hash},
25 },
26 rpc::helpers as rpc_helpers,
27};
28
29const INITIALIZE_EVENT_SIGNATURE_HASH: &str =
30 "dd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438";
31
32sol! {
36 struct InitializeEventData {
37 uint24 fee;
38 int24 tick_spacing;
39 address hooks;
40 uint160 sqrtPriceX96;
41 int24 tick;
42 }
43}
44
45pub fn parse_initialize_event_hypersync(log: HypersyncLog) -> anyhow::Result<PoolCreatedEvent> {
72 validate_event_signature_hash("InitializeEvent", INITIALIZE_EVENT_SIGNATURE_HASH, &log)?;
73
74 let block_number = extract_block_number(&log)?;
75
76 let pool_manager_address = Address::from_slice(
78 log.address
79 .clone()
80 .expect("PoolManager address should be set in logs")
81 .as_ref(),
82 );
83
84 let topics = &log.topics;
90 if topics.len() < 4 {
91 anyhow::bail!(
92 "Initialize event missing topics: expected 4, got {}",
93 topics.len()
94 );
95 }
96
97 let pool_id_bytes = topics[1]
99 .as_ref()
100 .ok_or_else(|| anyhow::anyhow!("Missing poolId topic"))?
101 .as_ref();
102 let pool_identifier = Ustr::from(format!("0x{}", hex::encode(pool_id_bytes)).as_str());
103
104 let currency0 = Address::from_slice(
105 topics[2]
106 .as_ref()
107 .ok_or_else(|| anyhow::anyhow!("Missing currency0 topic"))?
108 .as_ref()
109 .get(12..32)
110 .ok_or_else(|| anyhow::anyhow!("Invalid currency0 topic length"))?,
111 );
112
113 let currency1 = Address::from_slice(
114 topics[3]
115 .as_ref()
116 .ok_or_else(|| anyhow::anyhow!("Missing currency1 topic"))?
117 .as_ref()
118 .get(12..32)
119 .ok_or_else(|| anyhow::anyhow!("Invalid currency1 topic length"))?,
120 );
121
122 if let Some(data) = log.data {
123 let data_bytes = data.as_ref();
124
125 if data_bytes.len() < 160 {
127 anyhow::bail!(
128 "Initialize event data too short: expected at least 160 bytes, got {}",
129 data_bytes.len()
130 );
131 }
132
133 let decoded = <InitializeEventData as SolType>::abi_decode(data_bytes)
134 .map_err(|e| anyhow::anyhow!("Failed to decode initialize event data: {e}"))?;
135
136 let mut event = PoolCreatedEvent::new(
137 block_number,
138 currency0,
139 currency1,
140 pool_manager_address, PoolIdentifier::PoolId(pool_identifier), Some(decoded.fee.to::<u32>()),
143 Some(i32::try_from(decoded.tick_spacing)? as u32),
144 );
145
146 event.set_initialize_params(decoded.sqrtPriceX96, i32::try_from(decoded.tick)?);
147 event.set_hooks(decoded.hooks);
148
149 Ok(event)
150 } else {
151 Err(anyhow::anyhow!("Missing data in initialize event log"))
152 }
153}
154
155pub fn parse_initialize_event_rpc(log: &RpcLog) -> anyhow::Result<PoolCreatedEvent> {
161 rpc_helpers::validate_event_signature(log, INITIALIZE_EVENT_SIGNATURE_HASH, "InitializeEvent")?;
162
163 let block_number = rpc_helpers::extract_block_number(log)?;
164
165 let pool_manager_bytes = rpc_helpers::decode_hex(&log.address)?;
167 let pool_manager_address = Address::from_slice(&pool_manager_bytes);
168
169 if log.topics.len() < 4 {
175 anyhow::bail!(
176 "Initialize event missing topics: expected 4, got {}",
177 log.topics.len()
178 );
179 }
180
181 let pool_id_bytes = rpc_helpers::decode_hex(&log.topics[1])?;
183 let pool_identifier = Ustr::from(format!("0x{}", hex::encode(pool_id_bytes)).as_str());
184
185 let currency0_bytes = rpc_helpers::decode_hex(&log.topics[2])?;
186 let currency0 = Address::from_slice(¤cy0_bytes[12..32]);
187
188 let currency1_bytes = rpc_helpers::decode_hex(&log.topics[3])?;
189 let currency1 = Address::from_slice(¤cy1_bytes[12..32]);
190
191 let data_bytes = rpc_helpers::extract_data_bytes(log)?;
193
194 if data_bytes.len() < 160 {
196 anyhow::bail!(
197 "Initialize event data too short: expected at least 160 bytes, got {}",
198 data_bytes.len()
199 );
200 }
201
202 let decoded = <InitializeEventData as SolType>::abi_decode(&data_bytes)
203 .map_err(|e| anyhow::anyhow!("Failed to decode initialize event data: {e}"))?;
204
205 let mut event = PoolCreatedEvent::new(
206 block_number,
207 currency0,
208 currency1,
209 pool_manager_address,
210 PoolIdentifier::PoolId(pool_identifier), Some(decoded.fee.to::<u32>()),
212 Some(i32::try_from(decoded.tick_spacing)? as u32),
213 );
214
215 event.set_initialize_params(decoded.sqrtPriceX96, i32::try_from(decoded.tick)?);
216 event.set_hooks(decoded.hooks);
217
218 Ok(event)
219}
220
221#[cfg(test)]
222mod tests {
223 use rstest::{fixture, rstest};
224 use serde_json::json;
225
226 use super::*;
227
228 #[fixture]
235 fn hypersync_log_weth_usdc() -> HypersyncLog {
236 let log_json = json!({
237 "removed": null,
238 "log_index": "0x1",
239 "transaction_index": "0x3",
240 "transaction_hash": "0xdb973062b20333d61a57f4dc14b33c044e044a97c7d3db2900acc61e04179738",
241 "block_hash": null,
242 "block_number": "0x11c44853",
243 "address": "0x360e68faccca8ca495c1b759fd9eee466db9fb32",
244 "data": "0x0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e08ab0dd488513a6f62efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd0765",
245 "topics": [
246 "0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438",
247 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
248 "0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1",
249 "0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831"
250 ]
251 });
252 serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
253 }
254
255 #[fixture]
256 fn rpc_log_weth_usdc() -> RpcLog {
257 let log_json = json!({
258 "removed": false,
259 "logIndex": "0x1",
260 "transactionIndex": "0x3",
261 "transactionHash": "0xdb973062b20333d61a57f4dc14b33c044e044a97c7d3db2900acc61e04179738",
262 "blockHash": "0x4f72d534028d2322fa2dcaa3f470467a264eda2e20f73eeb1ece370361bb0ee7",
263 "blockNumber": "0x11c44853",
264 "address": "0x360e68faccca8ca495c1b759fd9eee466db9fb32",
265 "data": "0x0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e08ab0dd488513a6f62efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd0765",
266 "topics": [
267 "0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438",
268 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461",
269 "0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1",
270 "0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831"
271 ]
272 });
273 serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
274 }
275
276 #[rstest]
279 fn test_parse_initialize_hypersync(hypersync_log_weth_usdc: HypersyncLog) {
280 let event =
281 parse_initialize_event_hypersync(hypersync_log_weth_usdc).expect("Failed to parse");
282
283 assert_eq!(event.block_number, 298076243);
284 assert_eq!(
285 event.token0.to_string().to_lowercase(),
286 "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
287 );
288 assert_eq!(
289 event.token1.to_string().to_lowercase(),
290 "0xaf88d065e77c8cc2239327c5edb3a432268e5831"
291 );
292 assert_eq!(
293 event.pool_identifier.to_string(),
294 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461"
295 );
296 assert_eq!(event.fee, Some(3000));
297 assert_eq!(event.tick_spacing, Some(60));
298 }
299
300 #[rstest]
303 fn test_parse_initialize_rpc(rpc_log_weth_usdc: RpcLog) {
304 let event = parse_initialize_event_rpc(&rpc_log_weth_usdc).expect("Failed to parse");
305
306 assert_eq!(event.block_number, 298076243);
307 assert_eq!(
308 event.token0.to_string().to_lowercase(),
309 "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
310 );
311 assert_eq!(
312 event.token1.to_string().to_lowercase(),
313 "0xaf88d065e77c8cc2239327c5edb3a432268e5831"
314 );
315 assert_eq!(
316 event.pool_identifier.to_string(),
317 "0xc9bc8043294146424a4e4607d8ad837d6a659142822bbaaabc83bb57e7447461"
318 );
319 assert_eq!(event.fee, Some(3000));
320 assert_eq!(event.tick_spacing, Some(60));
321 }
322
323 #[rstest]
326 fn test_hypersync_rpc_match(hypersync_log_weth_usdc: HypersyncLog, rpc_log_weth_usdc: RpcLog) {
327 let hypersync_event =
328 parse_initialize_event_hypersync(hypersync_log_weth_usdc).expect("HyperSync parse");
329 let rpc_event = parse_initialize_event_rpc(&rpc_log_weth_usdc).expect("RPC parse");
330
331 assert_eq!(hypersync_event.block_number, rpc_event.block_number);
332 assert_eq!(hypersync_event.token0, rpc_event.token0);
333 assert_eq!(hypersync_event.token1, rpc_event.token1);
334 assert_eq!(hypersync_event.pool_identifier, rpc_event.pool_identifier);
335 assert_eq!(hypersync_event.fee, rpc_event.fee);
336 assert_eq!(hypersync_event.tick_spacing, rpc_event.tick_spacing);
337 }
338}