nautilus_blockchain/exchanges/parsing/uniswap_v3/
pool_created.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::primitives::{Address, U256};
17use nautilus_model::defi::{PoolIdentifier, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21    events::pool_created::PoolCreatedEvent,
22    exchanges::parsing::core,
23    hypersync::{
24        HypersyncLog,
25        helpers::{
26            extract_address_from_topic, extract_block_number, validate_event_signature_hash,
27        },
28    },
29    rpc::helpers as rpc_helpers,
30};
31
32const POOL_CREATED_EVENT_SIGNATURE_HASH: &str =
33    "783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118";
34
35/// Parses a pool creation event from a HyperSync log.
36///
37/// # Errors
38///
39/// Returns an error if the log parsing fails or if the event data is invalid.
40///
41/// # Panics
42///
43/// Panics if the block number is not set in the log.
44pub fn parse_pool_created_event_hypersync(log: HypersyncLog) -> anyhow::Result<PoolCreatedEvent> {
45    validate_event_signature_hash("PoolCreatedEvent", POOL_CREATED_EVENT_SIGNATURE_HASH, &log)?;
46
47    let block_number = extract_block_number(&log)?;
48
49    let token = extract_address_from_topic(&log, 1, "token0")?;
50    let token1 = extract_address_from_topic(&log, 2, "token1")?;
51
52    let fee = if let Some(topic) = log.topics.get(3).and_then(|t| t.as_ref()) {
53        U256::from_be_slice(topic.as_ref()).as_limbs()[0] as u32
54    } else {
55        anyhow::bail!("Missing fee in topic3 when parsing pool created event");
56    };
57
58    if let Some(data) = log.data {
59        // Data contains: [tick_spacing (32 bytes), pool_address (32 bytes)]
60        let data_bytes = data.as_ref();
61
62        // Extract tick_spacing (first 32 bytes)
63        let tick_spacing_bytes: [u8; 32] = data_bytes[0..32].try_into()?;
64        let tick_spacing = u32::from_be_bytes(tick_spacing_bytes[28..32].try_into()?);
65
66        // Extract pool_address (next 32 bytes)
67        let pool_address_bytes: [u8; 32] = data_bytes[32..64].try_into()?;
68        let pool_address = Address::from_slice(&pool_address_bytes[12..32]);
69
70        Ok(PoolCreatedEvent::new(
71            block_number,
72            token,
73            token1,
74            pool_address,
75            PoolIdentifier::Address(Ustr::from(&pool_address.to_string())), // For V2/V3, pool_identifier = pool_address
76            Some(fee),
77            Some(tick_spacing),
78        ))
79    } else {
80        Err(anyhow::anyhow!("Missing data in pool created event log"))
81    }
82}
83
84/// Parses a pool creation event from an RPC log.
85///
86/// # Errors
87///
88/// Returns an error if the log parsing fails or if the event data is invalid.
89pub fn parse_pool_created_event_rpc(log: &RpcLog) -> anyhow::Result<PoolCreatedEvent> {
90    rpc_helpers::validate_event_signature(
91        log,
92        POOL_CREATED_EVENT_SIGNATURE_HASH,
93        "PoolCreatedEvent",
94    )?;
95
96    let block_number = rpc_helpers::extract_block_number(log)?;
97    let token0 = rpc_helpers::extract_address_from_topic(log, 1, "token0")?;
98    let token1 = rpc_helpers::extract_address_from_topic(log, 2, "token1")?;
99
100    // Extract fee from topic3
101    let fee_bytes = rpc_helpers::extract_topic_bytes(log, 3)?;
102    let fee = core::extract_u32_from_bytes(&fee_bytes)?;
103
104    // Extract tick_spacing and pool from data
105    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
106
107    anyhow::ensure!(
108        data_bytes.len() >= 64,
109        "Pool created event data too short: expected at least 64 bytes, got {}",
110        data_bytes.len()
111    );
112
113    let tick_spacing = u32::from_be_bytes(data_bytes[28..32].try_into()?);
114    let pool_address = Address::from_slice(&data_bytes[44..64]);
115
116    Ok(PoolCreatedEvent::new(
117        block_number,
118        token0,
119        token1,
120        pool_address,
121        PoolIdentifier::Address(Ustr::from(&pool_address.to_string())), // For V2/V3, pool_identifier = pool_address
122        Some(fee),
123        Some(tick_spacing),
124    ))
125}
126
127#[cfg(test)]
128mod tests {
129    use rstest::{fixture, rstest};
130    use serde_json::json;
131
132    use super::*;
133
134    // ========== Block 185 fixtures ==========
135    // Pool: 0xB9Fc136980D98C034a529AadbD5651c087365D5f
136    // token0: 0x2E5353426C89F4eCD52D1036DA822D47E73376C4
137    // token1: 0x838930cFE7502dd36B0b1ebbef8001fbF94f3bFb
138    // fee: 3000, tickSpacing: 60
139
140    #[fixture]
141    fn hypersync_log_block_185() -> HypersyncLog {
142        let log_json = json!({
143            "removed": null,
144            "log_index": "0x0",
145            "transaction_index": "0x0",
146            "transaction_hash": "0x24058dde7caf5b8b70041de8b27731f20f927365f210247c3e720e947b9098e7",
147            "block_hash": null,
148            "block_number": "0xb9",
149            "address": "0x1f98431c8ad98523631ae4a59f267346ea31f984",
150            "data": "0x000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000b9fc136980d98c034a529aadbd5651c087365d5f",
151            "topics": [
152                "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
153                "0x0000000000000000000000002e5353426c89f4ecd52d1036da822d47e73376c4",
154                "0x000000000000000000000000838930cfe7502dd36b0b1ebbef8001fbf94f3bfb",
155                "0x0000000000000000000000000000000000000000000000000000000000000bb8"
156            ]
157        });
158        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
159    }
160
161    #[fixture]
162    fn rpc_log_block_185() -> RpcLog {
163        RpcLog {
164            removed: false,
165            log_index: Some("0x0".to_string()),
166            transaction_index: Some("0x0".to_string()),
167            transaction_hash: Some(
168                "0x24058dde7caf5b8b70041de8b27731f20f927365f210247c3e720e947b9098e7".to_string(),
169            ),
170            block_hash: Some(
171                "0xd371b6c7b04ec33d6470f067a82e87d7b294b952bea7a46d7b939b4c7addc275".to_string(),
172            ),
173            block_number: Some("0xb9".to_string()),
174            address: "0x1f98431c8ad98523631ae4a59f267346ea31f984".to_string(),
175            data: "0x000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000b9fc136980d98c034a529aadbd5651c087365d5f".to_string(),
176            topics: vec![
177                "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118".to_string(),
178                "0x0000000000000000000000002e5353426c89f4ecd52d1036da822d47e73376c4".to_string(),
179                "0x000000000000000000000000838930cfe7502dd36b0b1ebbef8001fbf94f3bfb".to_string(),
180                "0x0000000000000000000000000000000000000000000000000000000000000bb8".to_string(),
181            ],
182        }
183    }
184
185    // ========== Block 540 fixtures ==========
186    // Pool: 0x7d25DE0bB3e4E4d5F7b399db5A0BCa9F60dD66e4
187    // token0: 0x8dd7c686B11c115FfAbA245CBfc418B371087F68
188    // token1: 0xBE5381d826375492E55E05039a541eb2CB978e76
189    // fee: 500, tickSpacing: 10
190
191    #[fixture]
192    fn hypersync_log_block_540() -> HypersyncLog {
193        let log_json = json!({
194            "removed": null,
195            "log_index": "0x0",
196            "transaction_index": "0x0",
197            "transaction_hash": "0x0810b3488eba9b0264d3544b4548b70d0c8667e05ac4a5d90686f4a9f70509df",
198            "block_hash": null,
199            "block_number": "0x21c",
200            "address": "0x1f98431c8ad98523631ae4a59f267346ea31f984",
201            "data": "0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007d25de0bb3e4e4d5f7b399db5a0bca9f60dd66e4",
202            "topics": [
203                "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118",
204                "0x0000000000000000000000008dd7c686b11c115ffaba245cbfc418b371087f68",
205                "0x000000000000000000000000be5381d826375492e55e05039a541eb2cb978e76",
206                "0x00000000000000000000000000000000000000000000000000000000000001f4"
207            ]
208        });
209        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
210    }
211
212    #[fixture]
213    fn rpc_log_block_540() -> RpcLog {
214        RpcLog {
215            removed: false,
216            log_index: Some("0x0".to_string()),
217            transaction_index: Some("0x0".to_string()),
218            transaction_hash: Some(
219                "0x0810b3488eba9b0264d3544b4548b70d0c8667e05ac4a5d90686f4a9f70509df".to_string(),
220            ),
221            block_hash: Some(
222                "0x59bb10cdfd586affc6aa4a0b12f0662ec04599a1a459ac5b33129bc2c8705ccd".to_string(),
223            ),
224            block_number: Some("0x21c".to_string()),
225            address: "0x1f98431c8ad98523631ae4a59f267346ea31f984".to_string(),
226            data: "0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007d25de0bb3e4e4d5f7b399db5a0bca9f60dd66e4".to_string(),
227            topics: vec![
228                "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118".to_string(),
229                "0x0000000000000000000000008dd7c686b11c115ffaba245cbfc418b371087f68".to_string(),
230                "0x000000000000000000000000be5381d826375492e55e05039a541eb2cb978e76".to_string(),
231                "0x00000000000000000000000000000000000000000000000000000000000001f4".to_string(),
232            ],
233        }
234    }
235
236    // ========== HyperSync parser tests ==========
237
238    #[rstest]
239    fn test_parse_pool_created_hypersync_block_185(hypersync_log_block_185: HypersyncLog) {
240        let event =
241            parse_pool_created_event_hypersync(hypersync_log_block_185).expect("Failed to parse");
242
243        assert_eq!(event.block_number, 185);
244        assert_eq!(
245            event.token0.to_string().to_lowercase(),
246            "0x2e5353426c89f4ecd52d1036da822d47e73376c4"
247        );
248        assert_eq!(
249            event.token1.to_string().to_lowercase(),
250            "0x838930cfe7502dd36b0b1ebbef8001fbf94f3bfb"
251        );
252        assert_eq!(
253            event.pool_identifier.to_string(),
254            "0xB9Fc136980D98C034a529AadbD5651c087365D5f"
255        );
256        assert_eq!(event.fee, Some(3000));
257        assert_eq!(event.tick_spacing, Some(60));
258    }
259
260    #[rstest]
261    fn test_parse_pool_created_hypersync_block_540(hypersync_log_block_540: HypersyncLog) {
262        let event =
263            parse_pool_created_event_hypersync(hypersync_log_block_540).expect("Failed to parse");
264
265        assert_eq!(event.block_number, 540);
266        assert_eq!(
267            event.token0.to_string().to_lowercase(),
268            "0x8dd7c686b11c115ffaba245cbfc418b371087f68"
269        );
270        assert_eq!(
271            event.token1.to_string().to_lowercase(),
272            "0xbe5381d826375492e55e05039a541eb2cb978e76"
273        );
274        assert_eq!(
275            event.pool_identifier.to_string(),
276            "0x7d25DE0bB3e4E4d5F7b399db5A0BCa9F60dD66e4"
277        );
278        assert_eq!(event.fee, Some(500));
279        assert_eq!(event.tick_spacing, Some(10));
280    }
281
282    // ========== RPC parser tests ==========
283
284    #[rstest]
285    fn test_parse_pool_created_rpc_block_185(rpc_log_block_185: RpcLog) {
286        let event = parse_pool_created_event_rpc(&rpc_log_block_185).expect("Failed to parse");
287
288        assert_eq!(event.block_number, 185);
289        assert_eq!(
290            event.token0.to_string().to_lowercase(),
291            "0x2e5353426c89f4ecd52d1036da822d47e73376c4"
292        );
293        assert_eq!(
294            event.token1.to_string().to_lowercase(),
295            "0x838930cfe7502dd36b0b1ebbef8001fbf94f3bfb"
296        );
297        assert_eq!(
298            event.pool_identifier.to_string(),
299            "0xB9Fc136980D98C034a529AadbD5651c087365D5f"
300        );
301        assert_eq!(event.fee, Some(3000));
302        assert_eq!(event.tick_spacing, Some(60));
303    }
304
305    #[rstest]
306    fn test_parse_pool_created_rpc_block_540(rpc_log_block_540: RpcLog) {
307        let event = parse_pool_created_event_rpc(&rpc_log_block_540).expect("Failed to parse");
308
309        assert_eq!(event.block_number, 540);
310        assert_eq!(
311            event.token0.to_string().to_lowercase(),
312            "0x8dd7c686b11c115ffaba245cbfc418b371087f68"
313        );
314        assert_eq!(
315            event.token1.to_string().to_lowercase(),
316            "0xbe5381d826375492e55e05039a541eb2cb978e76"
317        );
318        assert_eq!(
319            event.pool_identifier.to_string(),
320            "0x7d25DE0bB3e4E4d5F7b399db5A0BCa9F60dD66e4"
321        );
322        assert_eq!(event.fee, Some(500));
323        assert_eq!(event.tick_spacing, Some(10));
324    }
325
326    // ========== Cross-validation tests ==========
327
328    #[rstest]
329    fn test_hypersync_rpc_match_block_185(
330        hypersync_log_block_185: HypersyncLog,
331        rpc_log_block_185: RpcLog,
332    ) {
333        let hypersync_event =
334            parse_pool_created_event_hypersync(hypersync_log_block_185).expect("HyperSync parse");
335        let rpc_event = parse_pool_created_event_rpc(&rpc_log_block_185).expect("RPC parse");
336
337        assert_eq!(hypersync_event.block_number, rpc_event.block_number);
338        assert_eq!(hypersync_event.token0, rpc_event.token0);
339        assert_eq!(hypersync_event.token1, rpc_event.token1);
340        assert_eq!(hypersync_event.pool_identifier, rpc_event.pool_identifier);
341        assert_eq!(hypersync_event.fee, rpc_event.fee);
342        assert_eq!(hypersync_event.tick_spacing, rpc_event.tick_spacing);
343    }
344
345    #[rstest]
346    fn test_hypersync_rpc_match_block_540(
347        hypersync_log_block_540: HypersyncLog,
348        rpc_log_block_540: RpcLog,
349    ) {
350        let hypersync_event =
351            parse_pool_created_event_hypersync(hypersync_log_block_540).expect("HyperSync parse");
352        let rpc_event = parse_pool_created_event_rpc(&rpc_log_block_540).expect("RPC parse");
353
354        assert_eq!(hypersync_event.block_number, rpc_event.block_number);
355        assert_eq!(hypersync_event.token0, rpc_event.token0);
356        assert_eq!(hypersync_event.token1, rpc_event.token1);
357        assert_eq!(hypersync_event.pool_identifier, rpc_event.pool_identifier);
358        assert_eq!(hypersync_event.fee, rpc_event.fee);
359        assert_eq!(hypersync_event.tick_spacing, rpc_event.tick_spacing);
360    }
361}