nautilus_blockchain/exchanges/parsing/uniswap_v4/
initialize.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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
32// Define sol macro for parsing Initialize event data
33// Topics contain: [signature, poolId, currency0, currency1]
34// Data contains 5 parameters: fee, tickSpacing, hooks, sqrtPriceX96, tick
35sol! {
36    struct InitializeEventData {
37        uint24 fee;
38        int24 tick_spacing;
39        address hooks;
40        uint160 sqrtPriceX96;
41        int24 tick;
42    }
43}
44
45/// Parses a UniswapV4 Initialize event from a HyperSync log.
46///
47/// UniswapV4 uses the Initialize event for pool discovery (no separate PoolCreated event).
48/// The PoolManager is a singleton contract that manages all V4 pools.
49///
50/// Initialize event signature:
51/// ```solidity
52/// event Initialize(
53///     PoolId indexed id,          // bytes32 (topic1)
54///     Currency indexed currency0, // address (topic2)
55///     Currency indexed currency1, // address (topic3)
56///     uint24 fee,                // (data)
57///     int24 tickSpacing,         // (data)
58///     IHooks hooks,              // address (data)
59///     uint160 sqrtPriceX96,      // (data)
60///     int24 tick                 // (data)
61/// );
62/// ```
63///
64/// # Errors
65///
66/// Returns an error if the log parsing fails or if the event data is invalid.
67///
68/// # Panics
69///
70/// Panics if the block number is not set in the log.
71pub 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    // The pool address for V4 is the PoolManager contract address (the event emitter)
77    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    // Extract currency0 and currency1 from topics
85    // topics[0] = event signature
86    // topics[1] = poolId (bytes32)
87    // topics[2] = currency0 (indexed)
88    // topics[3] = currency1 (indexed)
89    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    // Extract Pool ID from topics[1] - this is the unique identifier for V4 pools
98    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        // Validate minimum data length (5 fields × 32 bytes = 160 bytes)
126        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, // V4 pools are managed by PoolManager
141            PoolIdentifier::PoolId(pool_identifier), // Pool ID (bytes32 as hex string)
142            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
155/// Parses a UniswapV4 Initialize event from an RPC log.
156///
157/// # Errors
158///
159/// Returns an error if the log parsing fails or if the event data is invalid.
160pub 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    // Pool address is the PoolManager contract (event emitter)
166    let pool_manager_bytes = rpc_helpers::decode_hex(&log.address)?;
167    let pool_manager_address = Address::from_slice(&pool_manager_bytes);
168
169    // Extract currency0 and currency1 from topics
170    // topics[0] = event signature
171    // topics[1] = poolId (bytes32)
172    // topics[2] = currency0 (indexed)
173    // topics[3] = currency1 (indexed)
174    if log.topics.len() < 4 {
175        anyhow::bail!(
176            "Initialize event missing topics: expected 4, got {}",
177            log.topics.len()
178        );
179    }
180
181    // Extract Pool ID from topics[1] - this is the unique identifier for V4 pools
182    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(&currency0_bytes[12..32]);
187
188    let currency1_bytes = rpc_helpers::decode_hex(&log.topics[3])?;
189    let currency1 = Address::from_slice(&currency1_bytes[12..32]);
190
191    // Extract and decode event data
192    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
193
194    // Validate minimum data length (5 fields × 32 bytes = 160 bytes)
195    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), // Pool ID (bytes32 as hex string)
211        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    // Real UniswapV4 Initialize event from Arbitrum
229    // Pool Manager: 0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32
230    // WETH-USDC pool
231    // Block: 0x11c44853 (297879635)
232    // Tx: 0xdb973062b20333d61a57f4dc14b33c044e044a97c7d3db2900acc61e04179738
233
234    #[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    // ========== HyperSync parser tests ==========
277
278    #[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    // ========== RPC parser tests ==========
301
302    #[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    // ========== Cross-validation tests ==========
324
325    #[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}