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