Skip to main content

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