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