nautilus_blockchain/hypersync/
helpers.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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/// Extracts the transaction hash from a log entry
17///
18/// # Errors
19///
20/// Returns an error if the transaction hash is not present in the log.
21pub fn extract_transaction_hash(
22    log: &hypersync_client::simple_types::Log,
23) -> anyhow::Result<String> {
24    log.transaction_hash
25        .as_ref()
26        .map(ToString::to_string)
27        .ok_or_else(|| anyhow::anyhow!("Missing transaction hash in log"))
28}
29
30/// Extracts the transaction index from a log entry
31///
32/// # Errors
33///
34/// Returns an error if the transaction index is not present in the log.
35pub fn extract_transaction_index(log: &hypersync_client::simple_types::Log) -> anyhow::Result<u32> {
36    log.transaction_index
37        .as_ref()
38        .map(|index| **index as u32)
39        .ok_or_else(|| anyhow::anyhow!("Missing transaction index in the log"))
40}
41
42/// Extracts the log index from a log entry
43///
44/// # Errors
45///
46/// Returns an error if the log index is not present in the log.
47pub fn extract_log_index(log: &hypersync_client::simple_types::Log) -> anyhow::Result<u32> {
48    log.log_index
49        .as_ref()
50        .map(|index| **index as u32)
51        .ok_or_else(|| anyhow::anyhow!("Missing log index in the log"))
52}
53
54/// Extracts the block number from a log entry
55///
56/// # Errors
57///
58/// Returns an error if the block number is not present in the log.
59pub fn extract_block_number(log: &hypersync_client::simple_types::Log) -> anyhow::Result<u64> {
60    log.block_number
61        .as_ref()
62        .map(|number| **number)
63        .ok_or_else(|| anyhow::anyhow!("Missing block number in the log"))
64}
65
66/// Validates that a log entry corresponds to the expected event by comparing its topic0 with the provided event signature hash.
67pub fn validate_event_signature_hash(
68    event_name: &str,
69    event_signature_hash: &str,
70    log: &hypersync_client::simple_types::Log,
71) -> anyhow::Result<()> {
72    if let Some(topic) = log.topics.first().and_then(|t| t.as_ref()) {
73        if hex::encode(topic) != event_signature_hash {
74            anyhow::bail!("Invalid event signature for event '{event_name}'");
75        }
76    } else {
77        anyhow::bail!("Missing event signature in topic0 for event '{event_name}'");
78    }
79    Ok(())
80}
81
82#[cfg(test)]
83mod tests {
84    use hypersync_client::simple_types::Log;
85    use rstest::*;
86    use serde_json::json;
87
88    use super::*;
89
90    #[fixture]
91    fn swap_log_1() -> Log {
92        let log_json = json!({
93            "removed": null,
94            "log_index": null,
95            "transaction_index": null,
96            "transaction_hash": null,
97            "block_hash": null,
98            "block_number": "0x1581b7e",
99            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
100            "data": "0x",
101            "topics": [
102                "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
103                "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
104                "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
105                null
106            ]
107        });
108        serde_json::from_value(log_json).expect("Failed to deserialize log")
109    }
110
111    #[fixture]
112    fn swap_log_2() -> Log {
113        let log_json = json!({
114            "removed": null,
115            "log_index": null,
116            "transaction_index": null,
117            "transaction_hash": null,
118            "block_hash": null,
119            "block_number": "0x1581b82",
120            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
121            "data": "0x",
122            "topics": [
123                "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
124                "0x00000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af",
125                "0x000000000000000000000000f90321d0ecad58ab2b0c8c79db8aaeeefa023578",
126                null
127            ]
128        });
129        serde_json::from_value(log_json).expect("Failed to deserialize log")
130    }
131
132    #[fixture]
133    fn log_without_topics() -> Log {
134        let log_json = json!({
135            "removed": null,
136            "log_index": null,
137            "transaction_index": null,
138            "transaction_hash": null,
139            "block_hash": null,
140            "block_number": "0x1581b82",
141            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
142            "data": "0x",
143            "topics": []
144        });
145        serde_json::from_value(log_json).expect("Failed to deserialize log")
146    }
147
148    #[fixture]
149    fn log_with_none_topic0() -> Log {
150        let log_json = json!({
151            "removed": null,
152            "log_index": null,
153            "transaction_index": null,
154            "transaction_hash": null,
155            "block_hash": null,
156            "block_number": "0x1581b82",
157            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
158            "data": "0x",
159            "topics": [null]
160        });
161        serde_json::from_value(log_json).expect("Failed to deserialize log")
162    }
163
164    #[rstest]
165    fn test_validate_event_signature_hash_success(swap_log_1: Log) {
166        // The topic0 from swap_log_1 is the swap event signature
167        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
168
169        let result = validate_event_signature_hash("Swap", expected_hash, &swap_log_1);
170        assert!(result.is_ok());
171    }
172
173    #[rstest]
174    fn test_validate_event_signature_hash_success_log2(swap_log_2: Log) {
175        // The topic0 from swap_log_2 is also the swap event signature
176        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
177
178        let result = validate_event_signature_hash("Swap", expected_hash, &swap_log_2);
179        assert!(result.is_ok());
180    }
181
182    #[rstest]
183    fn test_validate_event_signature_hash_mismatch(swap_log_1: Log) {
184        // Using a different event signature (e.g., Transfer event)
185        let wrong_hash = "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
186
187        let result = validate_event_signature_hash("Transfer", wrong_hash, &swap_log_1);
188        assert!(result.is_err());
189        assert_eq!(
190            result.unwrap_err().to_string(),
191            "Invalid event signature for event 'Transfer'"
192        );
193    }
194
195    #[rstest]
196    fn test_validate_event_signature_hash_missing_topic0(log_without_topics: Log) {
197        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
198
199        let result = validate_event_signature_hash("Swap", expected_hash, &log_without_topics);
200        assert!(result.is_err());
201        assert_eq!(
202            result.unwrap_err().to_string(),
203            "Missing event signature in topic0 for event 'Swap'"
204        );
205    }
206
207    #[rstest]
208    fn test_validate_event_signature_hash_none_topic0(log_with_none_topic0: Log) {
209        let expected_hash = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
210
211        let result = validate_event_signature_hash("Swap", expected_hash, &log_with_none_topic0);
212        assert!(result.is_err());
213        assert_eq!(
214            result.unwrap_err().to_string(),
215            "Missing event signature in topic0 for event 'Swap'"
216        );
217    }
218
219    #[rstest]
220    fn test_extract_transaction_hash_success() {
221        let log_json = json!({
222            "removed": null,
223            "log_index": null,
224            "transaction_index": null,
225            "transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
226            "block_hash": null,
227            "block_number": "0x1581b82",
228            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
229            "data": "0x",
230            "topics": []
231        });
232        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
233
234        let result = extract_transaction_hash(&log);
235        assert!(result.is_ok());
236        assert_eq!(
237            result.unwrap(),
238            "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
239        );
240    }
241
242    #[rstest]
243    fn test_extract_transaction_hash_missing() {
244        let log_json = json!({
245            "removed": null,
246            "log_index": null,
247            "transaction_index": null,
248            "transaction_hash": null,
249            "block_hash": null,
250            "block_number": "0x1581b82",
251            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
252            "data": "0x",
253            "topics": []
254        });
255        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
256
257        let result = extract_transaction_hash(&log);
258        assert!(result.is_err());
259        assert_eq!(
260            result.unwrap_err().to_string(),
261            "Missing transaction hash in log"
262        );
263    }
264
265    #[rstest]
266    fn test_extract_transaction_index_success() {
267        let log_json = json!({
268            "removed": null,
269            "log_index": null,
270            "transaction_index": "0x5",
271            "transaction_hash": null,
272            "block_hash": null,
273            "block_number": "0x1581b82",
274            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
275            "data": "0x",
276            "topics": []
277        });
278        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
279
280        let result = extract_transaction_index(&log);
281        assert!(result.is_ok());
282        assert_eq!(result.unwrap(), 5u32);
283    }
284
285    #[rstest]
286    fn test_extract_transaction_index_missing() {
287        let log_json = json!({
288            "removed": null,
289            "log_index": null,
290            "transaction_index": null,
291            "transaction_hash": null,
292            "block_hash": null,
293            "block_number": "0x1581b82",
294            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
295            "data": "0x",
296            "topics": []
297        });
298        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
299
300        let result = extract_transaction_index(&log);
301        assert!(result.is_err());
302        assert_eq!(
303            result.unwrap_err().to_string(),
304            "Missing transaction index in the log"
305        );
306    }
307
308    #[rstest]
309    fn test_extract_log_index_success() {
310        let log_json = json!({
311            "removed": null,
312            "log_index": "0xa",
313            "transaction_index": null,
314            "transaction_hash": null,
315            "block_hash": null,
316            "block_number": "0x1581b82",
317            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
318            "data": "0x",
319            "topics": []
320        });
321        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
322
323        let result = extract_log_index(&log);
324        assert!(result.is_ok());
325        assert_eq!(result.unwrap(), 10u32);
326    }
327
328    #[rstest]
329    fn test_extract_log_index_missing() {
330        let log_json = json!({
331            "removed": null,
332            "log_index": null,
333            "transaction_index": null,
334            "transaction_hash": null,
335            "block_hash": null,
336            "block_number": "0x1581b82",
337            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
338            "data": "0x",
339            "topics": []
340        });
341        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
342
343        let result = extract_log_index(&log);
344        assert!(result.is_err());
345        assert_eq!(
346            result.unwrap_err().to_string(),
347            "Missing log index in the log"
348        );
349    }
350
351    #[rstest]
352    fn test_extract_block_number_success() {
353        let log_json = json!({
354            "removed": null,
355            "log_index": null,
356            "transaction_index": null,
357            "transaction_hash": null,
358            "block_hash": null,
359            "block_number": "0x1581b82",
360            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
361            "data": "0x",
362            "topics": []
363        });
364        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
365
366        let result = extract_block_number(&log);
367        assert!(result.is_ok());
368        assert_eq!(result.unwrap(), 22551426u64); // 0x1581b82 in decimal
369    }
370
371    #[rstest]
372    fn test_extract_block_number_missing() {
373        let log_json = json!({
374            "removed": null,
375            "log_index": null,
376            "transaction_index": null,
377            "transaction_hash": null,
378            "block_hash": null,
379            "block_number": null,
380            "address": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
381            "data": "0x",
382            "topics": []
383        });
384        let log: Log = serde_json::from_value(log_json).expect("Failed to deserialize log");
385
386        let result = extract_block_number(&log);
387        assert!(result.is_err());
388        assert_eq!(
389            result.unwrap_err().to_string(),
390            "Missing block number in the log"
391        );
392    }
393}