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 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
101pub 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
114pub 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 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 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 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); }
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 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 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 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 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 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}