nautilus_blockchain/hypersync/
helpers.rs1use alloy::primitives::Address;
17
18pub 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 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
41pub 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
55pub 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
67pub 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
79pub 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
91pub 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
106pub 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
121pub 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 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 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 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); }
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 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 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 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 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 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}