nautilus_blockchain/hypersync/
helpers.rs1use alloy::primitives::Address;
17use nautilus_core::hex;
18
19use super::HypersyncLog;
20use crate::exchanges::parsing::core;
21
22pub 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
40pub 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
52pub 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
64pub 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
76pub 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
88pub fn extract_event_signature(log: &HypersyncLog) -> anyhow::Result<String> {
94 extract_event_signature_bytes(log).map(hex::encode)
95}
96
97pub 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
110pub 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 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 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 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); }
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 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 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 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 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 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}