Skip to main content

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