nautilus_blockchain/exchanges/parsing/
core.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
16//! Shared core extraction functions for parsing event logs.
17//!
18//! These functions operate on raw bytes and are used by both HyperSync and RPC parsers
19//! to ensure consistent extraction logic.
20
21use alloy::primitives::Address;
22
23/// Extract address from 32-byte topic (address in last 20 bytes).
24///
25/// In Ethereum event logs, indexed address parameters are stored as 32-byte
26/// values with the 20-byte address right-aligned (padded with zeros on the left).
27///
28/// # Errors
29///
30/// Returns an error if the byte slice is shorter than 32 bytes.
31pub fn extract_address_from_bytes(bytes: &[u8]) -> anyhow::Result<Address> {
32    anyhow::ensure!(
33        bytes.len() >= 32,
34        "Topic must be at least 32 bytes, got {}",
35        bytes.len()
36    );
37    Ok(Address::from_slice(&bytes[12..32]))
38}
39
40/// Extract u32 from 32-byte topic (value in last 4 bytes, big-endian).
41///
42/// In Ethereum event logs, indexed numeric parameters are stored as 32-byte
43/// values with the number right-aligned in big-endian format.
44///
45/// # Errors
46///
47/// Returns an error if the byte slice is shorter than 32 bytes.
48pub fn extract_u32_from_bytes(bytes: &[u8]) -> anyhow::Result<u32> {
49    anyhow::ensure!(
50        bytes.len() >= 32,
51        "Topic must be at least 32 bytes, got {}",
52        bytes.len()
53    );
54    Ok(u32::from_be_bytes(bytes[28..32].try_into()?))
55}
56
57/// Extract i32 from 32-byte topic (value in last 4 bytes, big-endian, signed).
58///
59/// In Ethereum event logs, indexed signed numeric parameters (like tick values)
60/// are stored as 32-byte values with the number right-aligned in big-endian format.
61///
62/// # Errors
63///
64/// Returns an error if the byte slice is shorter than 32 bytes.
65pub fn extract_i32_from_bytes(bytes: &[u8]) -> anyhow::Result<i32> {
66    anyhow::ensure!(
67        bytes.len() >= 32,
68        "Topic must be at least 32 bytes, got {}",
69        bytes.len()
70    );
71    Ok(i32::from_be_bytes(bytes[28..32].try_into()?))
72}
73
74/// Validate event signature matches expected hash.
75///
76/// The first topic (topic0) of an Ethereum event log contains the keccak256 hash
77/// of the event signature. This function validates that the actual signature
78/// matches the expected one.
79///
80/// # Errors
81///
82/// Returns an error if the signatures don't match.
83pub fn validate_signature_bytes(
84    actual: &[u8],
85    expected_hex: &str,
86    event_name: &str,
87) -> anyhow::Result<()> {
88    let actual_hex = hex::encode(actual);
89    anyhow::ensure!(
90        actual_hex == expected_hex,
91        "Invalid event signature for '{event_name}': expected {expected_hex}, got {actual_hex}",
92    );
93    Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98    use rstest::rstest;
99
100    use super::*;
101
102    #[rstest]
103    fn test_extract_address_token0() {
104        // token0 address from PoolCreated event topic1 at block 185
105        let bytes = hex::decode("0000000000000000000000002e5353426c89f4ecd52d1036da822d47e73376c4")
106            .unwrap();
107
108        let address = extract_address_from_bytes(&bytes).unwrap();
109        assert_eq!(
110            address.to_string().to_lowercase(),
111            "0x2e5353426c89f4ecd52d1036da822d47e73376c4"
112        );
113    }
114
115    #[rstest]
116    fn test_extract_address_token1_block() {
117        // token1 address from PoolCreated event topic2 at block 185
118        let bytes = hex::decode("000000000000000000000000838930cfe7502dd36b0b1ebbef8001fbf94f3bfb")
119            .unwrap();
120
121        let address = extract_address_from_bytes(&bytes).unwrap();
122        assert_eq!(
123            address.to_string().to_lowercase(),
124            "0x838930cfe7502dd36b0b1ebbef8001fbf94f3bfb"
125        );
126    }
127
128    #[rstest]
129    fn test_extract_address_from_bytes_too_short() {
130        let bytes = vec![0u8; 31];
131        let result = extract_address_from_bytes(&bytes);
132        assert!(result.is_err());
133        assert!(
134            result
135                .unwrap_err()
136                .to_string()
137                .contains("Topic must be at least 32 bytes")
138        );
139    }
140
141    #[rstest]
142    fn test_extract_u32_fee_3000() {
143        let bytes = hex::decode("0000000000000000000000000000000000000000000000000000000000000bb8")
144            .unwrap();
145
146        let value = extract_u32_from_bytes(&bytes).unwrap();
147        assert_eq!(value, 3000);
148    }
149
150    #[rstest]
151    fn test_extract_u32_fee_500() {
152        let bytes = hex::decode("00000000000000000000000000000000000000000000000000000000000001f4")
153            .unwrap();
154
155        let value = extract_u32_from_bytes(&bytes).unwrap();
156        assert_eq!(value, 500);
157    }
158
159    #[rstest]
160    fn test_extract_i32_tick_spacing_60() {
161        let bytes = hex::decode("000000000000000000000000000000000000000000000000000000000000003c")
162            .unwrap();
163
164        let value = extract_i32_from_bytes(&bytes).unwrap();
165        assert_eq!(value, 60);
166    }
167
168    #[rstest]
169    fn test_extract_i32_tick_spacing_10() {
170        let bytes = hex::decode("000000000000000000000000000000000000000000000000000000000000000a")
171            .unwrap();
172
173        let value = extract_i32_from_bytes(&bytes).unwrap();
174        assert_eq!(value, 10);
175    }
176
177    #[rstest]
178    fn test_extract_i32_from_bytes_negative() {
179        let bytes = hex::decode("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc4")
180            .unwrap();
181
182        let value = extract_i32_from_bytes(&bytes).unwrap();
183        assert_eq!(value, -60);
184    }
185
186    #[rstest]
187    fn test_validate_signature_pool_created() {
188        let pool_created_signature =
189            hex::decode("783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118")
190                .unwrap();
191        let expected = "783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118";
192
193        let result = validate_signature_bytes(&pool_created_signature, expected, "PoolCreated");
194        assert!(result.is_ok());
195    }
196
197    #[rstest]
198    fn test_validate_signature_bytes_mismatch() {
199        let pool_created_signature =
200            hex::decode("783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118")
201                .unwrap();
202        let swap_expected = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
203
204        let result = validate_signature_bytes(&pool_created_signature, swap_expected, "Swap");
205        assert!(result.is_err());
206        assert!(
207            result
208                .unwrap_err()
209                .to_string()
210                .contains("Invalid event signature for 'Swap'")
211        );
212    }
213}