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