nautilus_blockchain/exchanges/parsing/uniswap_v2/
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;
17use nautilus_model::defi::{PoolIdentifier, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21    events::pool_created::PoolCreatedEvent,
22    hypersync::{
23        HypersyncLog,
24        helpers::{
25            extract_address_from_topic, extract_block_number, validate_event_signature_hash,
26        },
27    },
28    rpc::helpers as rpc_helpers,
29};
30
31const PAIR_CREATED_EVENT_SIGNATURE_HASH: &str =
32    "0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9";
33
34/// Parses a UniswapV2 PairCreated event from a HyperSync log.
35///
36/// UniswapV2 emits PairCreated with:
37/// - topic0: event signature
38/// - topic1: token0 (indexed)
39/// - topic2: token1 (indexed)
40/// - data: pair address (32 bytes) + pair count (32 bytes)
41///
42/// # Errors
43///
44/// Returns an error if the log parsing fails or if the event data is invalid.
45///
46/// # Panics
47///
48/// Panics if the block number is not set in the log.
49pub fn parse_pool_created_event_hypersync(log: HypersyncLog) -> anyhow::Result<PoolCreatedEvent> {
50    validate_event_signature_hash("PairCreatedEvent", PAIR_CREATED_EVENT_SIGNATURE_HASH, &log)?;
51
52    let block_number = extract_block_number(&log)?;
53    let token0 = extract_address_from_topic(&log, 1, "token0")?;
54    let token1 = extract_address_from_topic(&log, 2, "token1")?;
55
56    if let Some(data) = log.data {
57        // Data contains: [pair_address (32 bytes), pair_count (32 bytes)]
58        let data_bytes = data.as_ref();
59
60        anyhow::ensure!(
61            data_bytes.len() >= 32,
62            "PairCreated event data too short: expected at least 32 bytes, got {}",
63            data_bytes.len()
64        );
65
66        // Extract pair address (first 32 bytes, address is right-aligned)
67        let pair_address = Address::from_slice(&data_bytes[12..32]);
68        let pool_identifier = PoolIdentifier::Address(Ustr::from(&pair_address.to_string()));
69
70        Ok(PoolCreatedEvent::new(
71            block_number,
72            token0,
73            token1,
74            pair_address,
75            pool_identifier, // For V2/V3, pool_identifier = pool_address
76            None,            // V2 has no fee tiers (fixed 0.3%)
77            None,            // V2 has no tick spacing (CPAMM)
78        ))
79    } else {
80        Err(anyhow::anyhow!("Missing data in pair created event log"))
81    }
82}
83
84/// Parses a UniswapV2 PairCreated 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        PAIR_CREATED_EVENT_SIGNATURE_HASH,
93        "PairCreatedEvent",
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 pair address from data
101    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
102
103    anyhow::ensure!(
104        data_bytes.len() >= 32,
105        "PairCreated event data too short: expected at least 32 bytes, got {}",
106        data_bytes.len()
107    );
108
109    // Pair address is in the first 32 bytes (right-aligned)
110    let pair_address = Address::from_slice(&data_bytes[12..32]);
111    let pool_identifier = PoolIdentifier::Address(Ustr::from(&pair_address.to_string()));
112
113    Ok(PoolCreatedEvent::new(
114        block_number,
115        token0,
116        token1,
117        pair_address,
118        pool_identifier, // For V2/V3, pool_identifier = pool_address
119        None,            // V2 has no fee tiers
120        None,            // V2 has no tick spacing
121    ))
122}
123
124#[cfg(test)]
125mod tests {
126    use rstest::{fixture, rstest};
127    use serde_json::json;
128
129    use super::*;
130
131    // Real UniswapV2 PairCreated event from Arbitrum
132    // Pair: WETH-USDC
133    // Block: 0x8fcb296 (150582934)
134    // Tx: 0xe7b5c25477c6dd2425c4bc07547ffb2777e018a12eed1d348d7bf553913d97b7
135
136    #[fixture]
137    fn hypersync_log_weth_usdt() -> HypersyncLog {
138        let log_json = json!({
139            "removed": null,
140            "log_index": "0x0",
141            "transaction_index": "0x1",
142            "transaction_hash": "0xe7b5c25477c6dd2425c4bc07547ffb2777e018a12eed1d348d7bf553913d97b7",
143            "block_hash": null,
144            "block_number": "0x8fcb296",
145            "address": "0xf1d7cc64fb4452f05c498126312ebe29f30fbcf9",
146            "data": "0x000000000000000000000000f64dfe17c8b87f012fcf50fbda1d62bfa148366a0000000000000000000000000000000000000000000000000000000000000001",
147            "topics": [
148                "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9",
149                "0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1",
150                "0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831"
151            ]
152        });
153        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
154    }
155
156    #[fixture]
157    fn rpc_log_weth_usdt() -> RpcLog {
158        let log_json = json!({
159            "removed": false,
160            "logIndex": "0x0",
161            "transactionIndex": "0x1",
162            "transactionHash": "0xe7b5c25477c6dd2425c4bc07547ffb2777e018a12eed1d348d7bf553913d97b7",
163            "blockHash": "0x5053fe02da5bb0c2fc690a467c1cc36e791047fc48c3ea4fe8bbeed069f3f7ba",
164            "blockNumber": "0x8fcb296",
165            "address": "0xf1d7cc64fb4452f05c498126312ebe29f30fbcf9",
166            "data": "0x000000000000000000000000f64dfe17c8b87f012fcf50fbda1d62bfa148366a0000000000000000000000000000000000000000000000000000000000000001",
167            "topics": [
168                "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9",
169                "0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1",
170                "0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831"
171            ]
172        });
173        serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
174    }
175
176    // ========== HyperSync parser tests ==========
177
178    #[rstest]
179    fn test_parse_pair_created_hypersync(hypersync_log_weth_usdt: HypersyncLog) {
180        let event =
181            parse_pool_created_event_hypersync(hypersync_log_weth_usdt).expect("Failed to parse");
182
183        assert_eq!(event.block_number, 150778518);
184        assert_eq!(
185            event.token0.to_string().to_lowercase(),
186            "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
187        );
188        assert_eq!(
189            event.token1.to_string().to_lowercase(),
190            "0xaf88d065e77c8cc2239327c5edb3a432268e5831"
191        );
192        assert_eq!(
193            event.pool_identifier.to_string(),
194            "0xF64Dfe17C8b87F012FCf50FbDA1D62bfA148366a",
195        );
196        assert_eq!(event.fee, None);
197        assert_eq!(event.tick_spacing, None);
198    }
199
200    // ========== RPC parser tests ==========
201
202    #[rstest]
203    fn test_parse_pair_created_rpc(rpc_log_weth_usdt: RpcLog) {
204        let event = parse_pool_created_event_rpc(&rpc_log_weth_usdt).expect("Failed to parse");
205
206        assert_eq!(event.block_number, 150778518);
207        assert_eq!(
208            event.token0.to_string().to_lowercase(),
209            "0x82af49447d8a07e3bd95bd0d56f35241523fbab1"
210        );
211        assert_eq!(
212            event.token1.to_string().to_lowercase(),
213            "0xaf88d065e77c8cc2239327c5edb3a432268e5831"
214        );
215        assert_eq!(
216            event.pool_identifier.to_string(),
217            "0xF64Dfe17C8b87F012FCf50FbDA1D62bfA148366a"
218        );
219        assert_eq!(event.fee, None);
220        assert_eq!(event.tick_spacing, None);
221    }
222
223    #[rstest]
224    fn test_hypersync_rpc_match(hypersync_log_weth_usdt: HypersyncLog, rpc_log_weth_usdt: RpcLog) {
225        let hypersync_event =
226            parse_pool_created_event_hypersync(hypersync_log_weth_usdt).expect("HyperSync parse");
227        let rpc_event = parse_pool_created_event_rpc(&rpc_log_weth_usdt).expect("RPC parse");
228
229        assert_eq!(hypersync_event.block_number, rpc_event.block_number);
230        assert_eq!(hypersync_event.token0, rpc_event.token0);
231        assert_eq!(hypersync_event.token1, rpc_event.token1);
232        assert_eq!(hypersync_event.pool_identifier, rpc_event.pool_identifier);
233        assert_eq!(hypersync_event.fee, rpc_event.fee);
234        assert_eq!(hypersync_event.tick_spacing, rpc_event.tick_spacing);
235    }
236}