Skip to main content

nautilus_blockchain/exchanges/parsing/uniswap_v4/
initialize.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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_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
33// Define sol macro for parsing Initialize event data
34// Topics contain: [signature, poolId, currency0, currency1]
35// Data contains 5 parameters: fee, tickSpacing, hooks, sqrtPriceX96, tick
36sol! {
37    struct InitializeEventData {
38        uint24 fee;
39        int24 tick_spacing;
40        address hooks;
41        uint160 sqrtPriceX96;
42        int24 tick;
43    }
44}
45
46/// Parses a UniswapV4 Initialize event from a HyperSync log.
47///
48/// UniswapV4 uses the Initialize event for pool discovery (no separate PoolCreated event).
49/// The PoolManager is a singleton contract that manages all V4 pools.
50///
51/// Initialize event signature:
52/// ```solidity
53/// event Initialize(
54///     PoolId indexed id,          // bytes32 (topic1)
55///     Currency indexed currency0, // address (topic2)
56///     Currency indexed currency1, // address (topic3)
57///     uint24 fee,                // (data)
58///     int24 tickSpacing,         // (data)
59///     IHooks hooks,              // address (data)
60///     uint160 sqrtPriceX96,      // (data)
61///     int24 tick                 // (data)
62/// );
63/// ```
64///
65/// # Errors
66///
67/// Returns an error if the log parsing fails or if the event data is invalid.
68///
69/// # Panics
70///
71/// Panics if the log address is not set.
72pub 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    // The pool address for V4 is the PoolManager contract address (the event emitter)
78    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    // Extract currency0 and currency1 from topics
86    // topics[0] = event signature
87    // topics[1] = poolId (bytes32)
88    // topics[2] = currency0 (indexed)
89    // topics[3] = currency1 (indexed)
90    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    // Extract Pool ID from topics[1] - this is the unique identifier for V4 pools
99    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        // Validate minimum data length (5 fields × 32 bytes = 160 bytes)
127        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, // V4 pools are managed by PoolManager
142            PoolIdentifier::PoolId(pool_identifier), // Pool ID (bytes32 as hex string)
143            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
156/// Parses a UniswapV4 Initialize event from an RPC log.
157///
158/// # Errors
159///
160/// Returns an error if the log parsing fails or if the event data is invalid.
161pub 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    // Pool address is the PoolManager contract (event emitter)
167    let pool_manager_bytes = rpc_helpers::decode_hex(&log.address)?;
168    let pool_manager_address = Address::from_slice(&pool_manager_bytes);
169
170    // Extract currency0 and currency1 from topics
171    // topics[0] = event signature
172    // topics[1] = poolId (bytes32)
173    // topics[2] = currency0 (indexed)
174    // topics[3] = currency1 (indexed)
175    if log.topics.len() < 4 {
176        anyhow::bail!(
177            "Initialize event missing topics: expected 4, was {}",
178            log.topics.len()
179        );
180    }
181
182    // Extract Pool ID from topics[1] - this is the unique identifier for V4 pools
183    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(&currency0_bytes[12..32]);
188
189    let currency1_bytes = rpc_helpers::decode_hex(&log.topics[3])?;
190    let currency1 = Address::from_slice(&currency1_bytes[12..32]);
191
192    // Extract and decode event data
193    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
194
195    // Validate minimum data length (5 fields × 32 bytes = 160 bytes)
196    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), // Pool ID (bytes32 as hex string)
212        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    // Real UniswapV4 Initialize event from Arbitrum
230    // Pool Manager: 0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32
231    // WETH-USDC pool
232    // Block: 0x11c44853 (297879635)
233    // Tx: 0xdb973062b20333d61a57f4dc14b33c044e044a97c7d3db2900acc61e04179738
234
235    #[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}