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