Skip to main content

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