Skip to main content

nautilus_blockchain/hypersync/
helpers.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_core::hex;
18
19use super::HypersyncLog;
20use crate::exchanges::parsing::core;
21
22/// Extracts an address from a specific topic in a log entry
23///
24/// # Errors
25///
26/// Returns an error if the topic at the specified index is not present in the log.
27pub fn extract_address_from_topic(
28    log: &HypersyncLog,
29    topic_index: usize,
30    description: &str,
31) -> anyhow::Result<Address> {
32    match log.topics.get(topic_index).and_then(|t| t.as_ref()) {
33        Some(topic) => core::extract_address_from_bytes(topic.as_ref()),
34        None => {
35            anyhow::bail!("Missing {description} address in topic{topic_index} when parsing event")
36        }
37    }
38}
39
40/// Extracts the transaction hash from a log entry
41///
42/// # Errors
43///
44/// Returns an error if the transaction hash is not present in the log.
45pub fn extract_transaction_hash(log: &HypersyncLog) -> anyhow::Result<String> {
46    log.transaction_hash
47        .as_ref()
48        .map(ToString::to_string)
49        .ok_or_else(|| anyhow::anyhow!("Missing transaction hash in log"))
50}
51
52/// Extracts the transaction index from a log entry
53///
54/// # Errors
55///
56/// Returns an error if the transaction index is not present in the log.
57pub fn extract_transaction_index(log: &HypersyncLog) -> anyhow::Result<u32> {
58    log.transaction_index
59        .as_ref()
60        .map(|index| **index as u32)
61        .ok_or_else(|| anyhow::anyhow!("Missing transaction index in the log"))
62}
63
64/// Extracts the log index from a log entry
65///
66/// # Errors
67///
68/// Returns an error if the log index is not present in the log.
69pub fn extract_log_index(log: &HypersyncLog) -> anyhow::Result<u32> {
70    log.log_index
71        .as_ref()
72        .map(|index| **index as u32)
73        .ok_or_else(|| anyhow::anyhow!("Missing log index in the log"))
74}
75
76/// Extracts the block number from a log entry
77///
78/// # Errors
79///
80/// Returns an error if the block number is not present in the log.
81pub fn extract_block_number(log: &HypersyncLog) -> anyhow::Result<u64> {
82    log.block_number
83        .as_ref()
84        .map(|number| **number)
85        .ok_or_else(|| anyhow::anyhow!("Missing block number in the log"))
86}
87
88/// Extracts the event signature from a log entry and returns it as a hex string
89///
90/// # Errors
91///
92/// Returns an error if the event signature (topic0) is not present in the log.
93pub fn extract_event_signature(log: &HypersyncLog) -> anyhow::Result<String> {
94    extract_event_signature_bytes(log).map(hex::encode)
95}
96
97/// Extracts the event signature from a log entry and returns it as raw bytes
98///
99/// # Errors
100///
101/// Returns an error if the event signature (topic0) is not present in the log.
102pub fn extract_event_signature_bytes(log: &HypersyncLog) -> anyhow::Result<&[u8]> {
103    if let Some(topic) = log.topics.first().and_then(|t| t.as_ref()) {
104        Ok(topic.as_ref())
105    } else {
106        anyhow::bail!("Missing event signature in topic0");
107    }
108}
109
110/// Validates that a log entry corresponds to the expected event by comparing its topic0 with the provided event signature hash.
111///
112/// # Errors
113///
114/// Returns an error if the event signature doesn't match or if topic0 is missing.
115pub fn validate_event_signature_hash(
116    event_name: &str,
117    target_event_signature_hash: &str,
118    log: &HypersyncLog,
119) -> anyhow::Result<()> {
120    let sig_bytes = extract_event_signature_bytes(log)?;
121    core::validate_signature_bytes(sig_bytes, target_event_signature_hash, event_name)
122}
123
124#[cfg(test)]
125mod tests {
126    use rstest::*;
127    use serde_json::json;
128
129    use super::*;
130
131    #[fixture]
132    fn swap_log_1() -> HypersyncLog {
133        let log_json = json!({
134            "removed": null,
135            "log_index": null,
136            "transaction_index": null,
137            "transaction_hash": null,
138            "block_hash": null,
139            "block_number": "0x1581b7e",
140            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
141            "data": "0x",
142            "topics": [
143                "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
144                "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
145                "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
146                null
147            ]
148        });
149        serde_json::from_value(log_json).expect("Failed to deserialize log")
150    }
151
152    #[fixture]
153    fn swap_log_2() -> HypersyncLog {
154        let log_json = json!({
155            "removed": null,
156            "log_index": null,
157            "transaction_index": null,
158            "transaction_hash": null,
159            "block_hash": null,
160            "block_number": "0x1581b82",
161            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
162            "data": "0x",
163            "topics": [
164                "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
165                "0x00000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af",
166                "0x000000000000000000000000f90321d0ecad58ab2b0c8c79db8aaeeefa023578",
167                null
168            ]
169        });
170        serde_json::from_value(log_json).expect("Failed to deserialize log")
171    }
172
173    #[fixture]
174    fn log_without_topics() -> HypersyncLog {
175        let log_json = json!({
176            "removed": null,
177            "log_index": null,
178            "transaction_index": null,
179            "transaction_hash": null,
180            "block_hash": null,
181            "block_number": "0x1581b82",
182            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
183            "data": "0x",
184            "topics": []
185        });
186        serde_json::from_value(log_json).expect("Failed to deserialize log")
187    }
188
189    #[fixture]
190    fn log_with_none_topic0() -> HypersyncLog {
191        let log_json = json!({
192            "removed": null,
193            "log_index": null,
194            "transaction_index": null,
195            "transaction_hash": null,
196            "block_hash": null,
197            "block_number": "0x1581b82",
198            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
199            "data": "0x",
200            "topics": [null]
201        });
202        serde_json::from_value(log_json).expect("Failed to deserialize log")
203    }
204
205    #[rstest]
206    fn test_validate_event_signature_hash_success(swap_log_1: HypersyncLog) {
207        // The topic0 from swap_log_1 is the swap event signature
208        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
209
210        let result = validate_event_signature_hash("Swap", expected_hash, &swap_log_1);
211        assert!(result.is_ok());
212    }
213
214    #[rstest]
215    fn test_validate_event_signature_hash_success_log2(swap_log_2: HypersyncLog) {
216        // The topic0 from swap_log_2 is also the swap event signature
217        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
218
219        let result = validate_event_signature_hash("Swap", expected_hash, &swap_log_2);
220        assert!(result.is_ok());
221    }
222
223    #[rstest]
224    fn test_validate_event_signature_hash_mismatch(swap_log_1: HypersyncLog) {
225        // Using a different event signature (e.g., Transfer event)
226        let wrong_hash = "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
227
228        let result = validate_event_signature_hash("Transfer", wrong_hash, &swap_log_1);
229        assert!(result.is_err());
230        assert!(
231            result
232                .unwrap_err()
233                .to_string()
234                .contains("Invalid event signature for 'Transfer'")
235        );
236    }
237
238    #[rstest]
239    fn test_validate_event_signature_hash_missing_topic0(log_without_topics: HypersyncLog) {
240        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
241
242        let result = validate_event_signature_hash("Swap", expected_hash, &log_without_topics);
243        assert!(result.is_err());
244        assert_eq!(
245            result.unwrap_err().to_string(),
246            "Missing event signature in topic0"
247        );
248    }
249
250    #[rstest]
251    fn test_validate_event_signature_hash_none_topic0(log_with_none_topic0: HypersyncLog) {
252        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
253
254        let result = validate_event_signature_hash("Swap", expected_hash, &log_with_none_topic0);
255        assert!(result.is_err());
256        assert_eq!(
257            result.unwrap_err().to_string(),
258            "Missing event signature in topic0"
259        );
260    }
261
262    #[rstest]
263    fn test_extract_transaction_hash_success() {
264        let log_json = json!({
265            "removed": null,
266            "log_index": null,
267            "transaction_index": null,
268            "transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
269            "block_hash": null,
270            "block_number": "0x1581b82",
271            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
272            "data": "0x",
273            "topics": []
274        });
275        let log: HypersyncLog =
276            serde_json::from_value(log_json).expect("Failed to deserialize log");
277
278        let result = extract_transaction_hash(&log);
279        assert!(result.is_ok());
280        assert_eq!(
281            result.unwrap(),
282            "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
283        );
284    }
285
286    #[rstest]
287    fn test_extract_transaction_hash_missing() {
288        let log_json = json!({
289            "removed": null,
290            "log_index": null,
291            "transaction_index": null,
292            "transaction_hash": null,
293            "block_hash": null,
294            "block_number": "0x1581b82",
295            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
296            "data": "0x",
297            "topics": []
298        });
299        let log: HypersyncLog =
300            serde_json::from_value(log_json).expect("Failed to deserialize log");
301
302        let result = extract_transaction_hash(&log);
303        assert!(result.is_err());
304        assert_eq!(
305            result.unwrap_err().to_string(),
306            "Missing transaction hash in log"
307        );
308    }
309
310    #[rstest]
311    fn test_extract_transaction_index_success() {
312        let log_json = json!({
313            "removed": null,
314            "log_index": null,
315            "transaction_index": "0x5",
316            "transaction_hash": null,
317            "block_hash": null,
318            "block_number": "0x1581b82",
319            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
320            "data": "0x",
321            "topics": []
322        });
323        let log: HypersyncLog =
324            serde_json::from_value(log_json).expect("Failed to deserialize log");
325
326        let result = extract_transaction_index(&log);
327        assert!(result.is_ok());
328        assert_eq!(result.unwrap(), 5u32);
329    }
330
331    #[rstest]
332    fn test_extract_transaction_index_missing() {
333        let log_json = json!({
334            "removed": null,
335            "log_index": null,
336            "transaction_index": null,
337            "transaction_hash": null,
338            "block_hash": null,
339            "block_number": "0x1581b82",
340            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
341            "data": "0x",
342            "topics": []
343        });
344        let log: HypersyncLog =
345            serde_json::from_value(log_json).expect("Failed to deserialize log");
346
347        let result = extract_transaction_index(&log);
348        assert!(result.is_err());
349        assert_eq!(
350            result.unwrap_err().to_string(),
351            "Missing transaction index in the log"
352        );
353    }
354
355    #[rstest]
356    fn test_extract_log_index_success() {
357        let log_json = json!({
358            "removed": null,
359            "log_index": "0xa",
360            "transaction_index": null,
361            "transaction_hash": null,
362            "block_hash": null,
363            "block_number": "0x1581b82",
364            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
365            "data": "0x",
366            "topics": []
367        });
368        let log: HypersyncLog =
369            serde_json::from_value(log_json).expect("Failed to deserialize log");
370
371        let result = extract_log_index(&log);
372        assert!(result.is_ok());
373        assert_eq!(result.unwrap(), 10u32);
374    }
375
376    #[rstest]
377    fn test_extract_log_index_missing() {
378        let log_json = json!({
379            "removed": null,
380            "log_index": null,
381            "transaction_index": null,
382            "transaction_hash": null,
383            "block_hash": null,
384            "block_number": "0x1581b82",
385            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
386            "data": "0x",
387            "topics": []
388        });
389        let log: HypersyncLog =
390            serde_json::from_value(log_json).expect("Failed to deserialize log");
391
392        let result = extract_log_index(&log);
393        assert!(result.is_err());
394        assert_eq!(
395            result.unwrap_err().to_string(),
396            "Missing log index in the log"
397        );
398    }
399
400    #[rstest]
401    fn test_extract_block_number_success() {
402        let log_json = json!({
403            "removed": null,
404            "log_index": null,
405            "transaction_index": null,
406            "transaction_hash": null,
407            "block_hash": null,
408            "block_number": "0x1581b82",
409            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
410            "data": "0x",
411            "topics": []
412        });
413        let log: HypersyncLog =
414            serde_json::from_value(log_json).expect("Failed to deserialize log");
415
416        let result = extract_block_number(&log);
417        assert!(result.is_ok());
418        assert_eq!(result.unwrap(), 22551426u64); // 0x1581b82 in decimal
419    }
420
421    #[rstest]
422    fn test_extract_block_number_missing() {
423        let log_json = json!({
424            "removed": null,
425            "log_index": null,
426            "transaction_index": null,
427            "transaction_hash": null,
428            "block_hash": null,
429            "block_number": null,
430            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
431            "data": "0x",
432            "topics": []
433        });
434        let log: HypersyncLog =
435            serde_json::from_value(log_json).expect("Failed to deserialize log");
436
437        let result = extract_block_number(&log);
438        assert!(result.is_err());
439        assert_eq!(
440            result.unwrap_err().to_string(),
441            "Missing block number in the log"
442        );
443    }
444
445    #[rstest]
446    fn test_extract_address_from_topic_success(swap_log_1: HypersyncLog) {
447        // Extract sender address from topic1
448        let result = extract_address_from_topic(&swap_log_1, 1, "sender");
449        assert!(result.is_ok());
450        let address = result.unwrap();
451        assert_eq!(
452            address.to_string().to_lowercase(),
453            "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad"
454        );
455    }
456
457    #[rstest]
458    fn test_extract_address_from_topic_success_log2(swap_log_2: HypersyncLog) {
459        // Extract sender address from topic1
460        let result = extract_address_from_topic(&swap_log_2, 1, "sender");
461        assert!(result.is_ok());
462        let address = result.unwrap();
463        assert_eq!(
464            address.to_string().to_lowercase(),
465            "0x66a9893cc07d91d95644aedd05d03f95e1dba8af"
466        );
467
468        // Extract recipient address from topic2
469        let result = extract_address_from_topic(&swap_log_2, 2, "recipient");
470        assert!(result.is_ok());
471        let address = result.unwrap();
472        assert_eq!(
473            address.to_string().to_lowercase(),
474            "0xf90321d0ecad58ab2b0c8c79db8aaeeefa023578"
475        );
476    }
477
478    #[rstest]
479    fn test_extract_address_from_topic_missing_topic(swap_log_1: HypersyncLog) {
480        // Try to extract from topic index 5 (doesn't exist)
481        let result = extract_address_from_topic(&swap_log_1, 5, "nonexistent");
482        assert!(result.is_err());
483        assert_eq!(
484            result.unwrap_err().to_string(),
485            "Missing nonexistent address in topic5 when parsing event"
486        );
487    }
488
489    #[rstest]
490    fn test_extract_address_from_topic_none_topic(swap_log_1: HypersyncLog) {
491        // Try to extract from topic index 3 (which is null in swap_log_1)
492        let result = extract_address_from_topic(&swap_log_1, 3, "null_topic");
493        assert!(result.is_err());
494        assert_eq!(
495            result.unwrap_err().to_string(),
496            "Missing null_topic address in topic3 when parsing event"
497        );
498    }
499
500    #[rstest]
501    fn test_extract_address_from_topic_no_topics(log_without_topics: HypersyncLog) {
502        let result = extract_address_from_topic(&log_without_topics, 1, "sender");
503        assert!(result.is_err());
504        assert_eq!(
505            result.unwrap_err().to_string(),
506            "Missing sender address in topic1 when parsing event"
507        );
508    }
509}