1use crate::constants::{
8 HEALTH_CHECK_ACTION_KEY, HEALTH_CHECK_ACTION_NONCE_HEALTH, HEALTH_CHECK_NONCE_HINT_KEY,
9};
10use crate::models::{NetworkType, WebhookNotification};
11use chrono::Utc;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use strum::Display;
15use uuid::Uuid;
16
17#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct Job<T> {
20 pub message_id: String,
21 pub version: String,
22 pub timestamp: String,
23 pub job_type: JobType,
24 pub data: T,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub request_id: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub available_at: Option<String>,
33}
34
35impl<T> Job<T> {
36 pub fn new(job_type: JobType, data: T) -> Self {
37 Self {
38 message_id: Uuid::new_v4().to_string(),
39 version: "1.0".to_string(),
40 timestamp: Utc::now().timestamp().to_string(),
41 job_type,
42 data,
43 request_id: None,
44 available_at: None,
45 }
46 }
47 pub fn with_request_id(mut self, id: Option<String>) -> Self {
48 self.request_id = id;
49 self
50 }
51 pub fn with_scheduled_on(mut self, scheduled_on: Option<i64>) -> Self {
52 self.available_at = scheduled_on.map(|ts| ts.to_string());
53 self
54 }
55}
56
57#[derive(Debug, Serialize, Deserialize, Display, Clone)]
59#[serde(tag = "type", rename_all = "snake_case")]
60pub enum JobType {
61 TransactionRequest,
62 TransactionSend,
63 TransactionStatusCheck,
64 NotificationSend,
65 TokenSwapRequest,
66 RelayerHealthCheck,
67}
68
69#[derive(Debug, Serialize, Deserialize, Clone)]
71pub struct TransactionRequest {
72 pub transaction_id: String,
73 pub relayer_id: String,
74 #[serde(default)]
79 pub network_type: Option<NetworkType>,
80 pub metadata: Option<HashMap<String, String>>,
81}
82
83impl TransactionRequest {
84 pub fn new(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
85 Self {
86 transaction_id: transaction_id.into(),
87 relayer_id: relayer_id.into(),
88 network_type: None,
89 metadata: None,
90 }
91 }
92
93 pub fn with_network_type(mut self, network_type: NetworkType) -> Self {
94 self.network_type = Some(network_type);
95 self
96 }
97
98 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
99 self.metadata = Some(metadata);
100 self
101 }
102}
103
104#[derive(Debug, Serialize, Deserialize, Clone)]
105pub enum TransactionCommand {
106 Submit,
107 Cancel { reason: String },
108 Resubmit,
109 Resend,
110}
111
112#[derive(Debug, Serialize, Deserialize, Clone)]
114pub struct TransactionSend {
115 pub transaction_id: String,
116 pub relayer_id: String,
117 pub command: TransactionCommand,
118 #[serde(default)]
123 pub network_type: Option<NetworkType>,
124 pub metadata: Option<HashMap<String, String>>,
125}
126
127impl TransactionSend {
128 pub fn submit(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
130 Self {
131 transaction_id: transaction_id.into(),
132 relayer_id: relayer_id.into(),
133 command: TransactionCommand::Submit,
134 network_type: None,
135 metadata: None,
136 }
137 }
138
139 pub fn cancel(
141 transaction_id: impl Into<String>,
142 relayer_id: impl Into<String>,
143 reason: impl Into<String>,
144 ) -> Self {
145 Self {
146 transaction_id: transaction_id.into(),
147 relayer_id: relayer_id.into(),
148 command: TransactionCommand::Cancel {
149 reason: reason.into(),
150 },
151 network_type: None,
152 metadata: None,
153 }
154 }
155
156 pub fn resubmit(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
158 Self {
159 transaction_id: transaction_id.into(),
160 relayer_id: relayer_id.into(),
161 command: TransactionCommand::Resubmit,
162 network_type: None,
163 metadata: None,
164 }
165 }
166
167 pub fn resend(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
169 Self {
170 transaction_id: transaction_id.into(),
171 relayer_id: relayer_id.into(),
172 command: TransactionCommand::Resend,
173 network_type: None,
174 metadata: None,
175 }
176 }
177
178 pub fn with_network_type(mut self, network_type: NetworkType) -> Self {
180 self.network_type = Some(network_type);
181 self
182 }
183
184 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
186 self.metadata = Some(metadata);
187 self
188 }
189}
190
191#[derive(Debug, Serialize, Deserialize, Clone)]
193pub struct TransactionStatusCheck {
194 pub transaction_id: String,
195 pub relayer_id: String,
196 #[serde(default)]
199 pub network_type: Option<NetworkType>,
200 pub metadata: Option<HashMap<String, String>>,
201}
202
203impl TransactionStatusCheck {
204 pub fn new(
206 transaction_id: impl Into<String>,
207 relayer_id: impl Into<String>,
208 network_type: NetworkType,
209 ) -> Self {
210 Self {
211 transaction_id: transaction_id.into(),
212 relayer_id: relayer_id.into(),
213 network_type: Some(network_type),
214 metadata: None,
215 }
216 }
217
218 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
220 self.metadata = Some(metadata);
221 self
222 }
223}
224
225#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
226pub struct NotificationSend {
227 pub notification_id: String,
228 pub notification: WebhookNotification,
229}
230
231impl NotificationSend {
232 pub fn new(notification_id: String, notification: WebhookNotification) -> Self {
233 Self {
234 notification_id,
235 notification,
236 }
237 }
238}
239
240#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
241pub struct TokenSwapRequest {
242 pub relayer_id: String,
243}
244
245impl TokenSwapRequest {
246 pub fn new(relayer_id: String) -> Self {
247 Self { relayer_id }
248 }
249}
250
251#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
252pub struct RelayerHealthCheck {
253 pub relayer_id: String,
254 pub retry_count: u32,
255 #[serde(default)]
258 pub metadata: Option<HashMap<String, String>>,
259}
260
261impl RelayerHealthCheck {
262 pub fn new(relayer_id: String) -> Self {
263 Self {
264 relayer_id,
265 retry_count: 0,
266 metadata: None,
267 }
268 }
269
270 pub fn with_retry_count(relayer_id: String, retry_count: u32) -> Self {
271 Self {
272 relayer_id,
273 retry_count,
274 metadata: None,
275 }
276 }
277
278 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
279 self.metadata = Some(metadata);
280 self
281 }
282
283 pub fn nonce_health(relayer_id: String) -> Self {
286 let mut metadata = HashMap::new();
287 metadata.insert(
288 HEALTH_CHECK_ACTION_KEY.to_string(),
289 HEALTH_CHECK_ACTION_NONCE_HEALTH.to_string(),
290 );
291 Self::new(relayer_id).with_metadata(metadata)
292 }
293
294 pub fn nonce_health_with_hint(relayer_id: String, nonce_hint: u64) -> Self {
298 let mut job = Self::nonce_health(relayer_id);
299 if let Some(ref mut metadata) = job.metadata {
300 metadata.insert(
301 HEALTH_CHECK_NONCE_HINT_KEY.to_string(),
302 nonce_hint.to_string(),
303 );
304 }
305 job
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use std::collections::HashMap;
312 use std::str::FromStr;
313
314 use crate::models::{
315 evm::Speed, EvmTransactionDataSignature, EvmTransactionResponse, TransactionResponse,
316 TransactionStatus, WebhookNotification, WebhookPayload, U256,
317 };
318
319 use super::*;
320
321 #[test]
322 fn test_job_creation() {
323 let job_data = TransactionRequest::new("tx123", "relayer-1");
324 let job = Job::new(JobType::TransactionRequest, job_data.clone());
325
326 assert_eq!(job.job_type.to_string(), "TransactionRequest");
327 assert_eq!(job.version, "1.0");
328 assert_eq!(job.data.transaction_id, "tx123");
329 assert_eq!(job.data.relayer_id, "relayer-1");
330 assert!(job.data.metadata.is_none());
331 }
332
333 #[test]
334 fn test_transaction_request_with_metadata() {
335 let mut metadata = HashMap::new();
336 metadata.insert("chain_id".to_string(), "1".to_string());
337 metadata.insert("gas_price".to_string(), "20000000000".to_string());
338
339 let tx_request =
340 TransactionRequest::new("tx123", "relayer-1").with_metadata(metadata.clone());
341
342 assert_eq!(tx_request.transaction_id, "tx123");
343 assert_eq!(tx_request.relayer_id, "relayer-1");
344 assert!(tx_request.metadata.is_some());
345 assert_eq!(tx_request.metadata.unwrap(), metadata);
346 }
347
348 #[test]
349 fn test_transaction_send_methods() {
350 let tx_submit = TransactionSend::submit("tx123", "relayer-1");
352 assert_eq!(tx_submit.transaction_id, "tx123");
353 assert_eq!(tx_submit.relayer_id, "relayer-1");
354 matches!(tx_submit.command, TransactionCommand::Submit);
355
356 let tx_cancel = TransactionSend::cancel("tx123", "relayer-1", "user requested");
358 matches!(tx_cancel.command, TransactionCommand::Cancel { reason } if reason == "user requested");
359
360 let tx_resubmit = TransactionSend::resubmit("tx123", "relayer-1");
362 matches!(tx_resubmit.command, TransactionCommand::Resubmit);
363
364 let tx_resend = TransactionSend::resend("tx123", "relayer-1");
366 matches!(tx_resend.command, TransactionCommand::Resend);
367
368 let mut metadata = HashMap::new();
370 metadata.insert("nonce".to_string(), "5".to_string());
371
372 let tx_with_metadata =
373 TransactionSend::submit("tx123", "relayer-1").with_metadata(metadata.clone());
374
375 assert!(tx_with_metadata.metadata.is_some());
376 assert_eq!(tx_with_metadata.metadata.unwrap(), metadata);
377 }
378
379 #[test]
380 fn test_transaction_status_check() {
381 let tx_status = TransactionStatusCheck::new("tx123", "relayer-1", NetworkType::Evm);
382 assert_eq!(tx_status.transaction_id, "tx123");
383 assert_eq!(tx_status.relayer_id, "relayer-1");
384 assert_eq!(tx_status.network_type, Some(NetworkType::Evm));
385 assert!(tx_status.metadata.is_none());
386
387 let mut metadata = HashMap::new();
388 metadata.insert("retries".to_string(), "3".to_string());
389
390 let tx_status_with_metadata =
391 TransactionStatusCheck::new("tx123", "relayer-1", NetworkType::Stellar)
392 .with_metadata(metadata.clone());
393
394 assert!(tx_status_with_metadata.metadata.is_some());
395 assert_eq!(tx_status_with_metadata.metadata.unwrap(), metadata);
396 }
397
398 #[test]
399 fn test_transaction_status_check_backward_compatibility() {
400 let old_json = r#"{
402 "transaction_id": "tx456",
403 "relayer_id": "relayer-2",
404 "metadata": null
405 }"#;
406
407 let deserialized: TransactionStatusCheck = serde_json::from_str(old_json).unwrap();
409 assert_eq!(deserialized.transaction_id, "tx456");
410 assert_eq!(deserialized.relayer_id, "relayer-2");
411 assert_eq!(deserialized.network_type, None);
412 assert!(deserialized.metadata.is_none());
413
414 let new_status = TransactionStatusCheck::new("tx789", "relayer-3", NetworkType::Solana);
416 assert_eq!(new_status.network_type, Some(NetworkType::Solana));
417 }
418
419 #[test]
420 fn test_job_serialization() {
421 let tx_request = TransactionRequest::new("tx123", "relayer-1");
422 let job = Job::new(JobType::TransactionRequest, tx_request);
423
424 let serialized = serde_json::to_string(&job).unwrap();
425 let deserialized: Job<TransactionRequest> = serde_json::from_str(&serialized).unwrap();
426
427 assert_eq!(deserialized.job_type.to_string(), "TransactionRequest");
428 assert_eq!(deserialized.data.transaction_id, "tx123");
429 assert_eq!(deserialized.data.relayer_id, "relayer-1");
430 }
431
432 #[test]
433 fn test_job_with_scheduled_on_sets_available_at() {
434 let tx_request = TransactionRequest::new("tx123", "relayer-1");
435 let job = Job::new(JobType::TransactionRequest, tx_request).with_scheduled_on(Some(12345));
436
437 assert_eq!(job.available_at.as_deref(), Some("12345"));
438 }
439
440 #[test]
441 fn test_job_serialization_preserves_available_at() {
442 let tx_request = TransactionRequest::new("tx123", "relayer-1");
443 let job = Job::new(JobType::TransactionRequest, tx_request).with_scheduled_on(Some(12345));
444
445 let serialized = serde_json::to_string(&job).unwrap();
446 let deserialized: Job<TransactionRequest> = serde_json::from_str(&serialized).unwrap();
447
448 assert_eq!(deserialized.available_at.as_deref(), Some("12345"));
449 }
450
451 #[test]
452 fn test_job_serialization_omits_available_at_when_not_scheduled() {
453 let tx_request = TransactionRequest::new("tx123", "relayer-1");
454 let job = Job::new(JobType::TransactionRequest, tx_request);
455
456 let serialized = serde_json::to_string(&job).unwrap();
457
458 assert!(!serialized.contains("available_at"));
459 }
460
461 #[test]
462 fn test_notification_send_serialization() {
463 let payload = WebhookPayload::Transaction(TransactionResponse::Evm(Box::new(
464 EvmTransactionResponse {
465 id: "tx123".to_string(),
466 hash: Some("0x123".to_string()),
467 status: TransactionStatus::Confirmed,
468 status_reason: None,
469 created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
470 sent_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
471 confirmed_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
472 gas_price: Some(1000000000),
473 gas_limit: Some(21000),
474 nonce: Some(1),
475 value: U256::from_str("1000000000000000000").unwrap(),
476 from: "0xabc".to_string(),
477 to: Some("0xdef".to_string()),
478 relayer_id: "relayer-1".to_string(),
479 data: Some("0x123".to_string()),
480 max_fee_per_gas: Some(1000000000),
481 max_priority_fee_per_gas: Some(1000000000),
482 signature: Some(EvmTransactionDataSignature {
483 r: "0x123".to_string(),
484 s: "0x123".to_string(),
485 v: 1,
486 sig: "0x123".to_string(),
487 }),
488 speed: Some(Speed::Fast),
489 is_canceled: None,
490 },
491 )));
492
493 let notification = WebhookNotification::new("transaction".to_string(), payload);
494 let notification_send =
495 NotificationSend::new("notification-test".to_string(), notification);
496
497 let serialized = serde_json::to_string(¬ification_send).unwrap();
498
499 match serde_json::from_str::<NotificationSend>(&serialized) {
500 Ok(deserialized) => {
501 assert_eq!(notification_send, deserialized);
502 }
503 Err(e) => {
504 panic!("Deserialization error: {e}");
505 }
506 }
507 }
508
509 #[test]
510 fn test_notification_send_serialization_none_values() {
511 let payload = WebhookPayload::Transaction(TransactionResponse::Evm(Box::new(
512 EvmTransactionResponse {
513 id: "tx123".to_string(),
514 hash: None,
515 status: TransactionStatus::Confirmed,
516 status_reason: None,
517 created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
518 sent_at: None,
519 confirmed_at: None,
520 gas_price: None,
521 gas_limit: Some(21000),
522 nonce: None,
523 value: U256::from_str("1000000000000000000").unwrap(),
524 from: "0xabc".to_string(),
525 to: None,
526 relayer_id: "relayer-1".to_string(),
527 data: None,
528 max_fee_per_gas: None,
529 max_priority_fee_per_gas: None,
530 signature: None,
531 speed: None,
532 is_canceled: None,
533 },
534 )));
535
536 let notification = WebhookNotification::new("transaction".to_string(), payload);
537 let notification_send =
538 NotificationSend::new("notification-test".to_string(), notification);
539
540 let serialized = serde_json::to_string(¬ification_send).unwrap();
541
542 match serde_json::from_str::<NotificationSend>(&serialized) {
543 Ok(deserialized) => {
544 assert_eq!(notification_send, deserialized);
545 }
546 Err(e) => {
547 panic!("Deserialization error: {e}");
548 }
549 }
550 }
551
552 #[test]
553 fn test_relayer_health_check_new() {
554 let health_check = RelayerHealthCheck::new("relayer-1".to_string());
555
556 assert_eq!(health_check.relayer_id, "relayer-1");
557 assert_eq!(health_check.retry_count, 0);
558 }
559
560 #[test]
561 fn test_relayer_health_check_with_retry_count() {
562 let health_check = RelayerHealthCheck::with_retry_count("relayer-1".to_string(), 5);
563
564 assert_eq!(health_check.relayer_id, "relayer-1");
565 assert_eq!(health_check.retry_count, 5);
566 }
567
568 #[test]
569 fn test_relayer_health_check_nonce_health() {
570 let job = RelayerHealthCheck::nonce_health("relayer-1".to_string());
571
572 assert_eq!(job.relayer_id, "relayer-1");
573 let metadata = job.metadata.as_ref().unwrap();
574 assert_eq!(
575 metadata.get(HEALTH_CHECK_ACTION_KEY),
576 Some(&HEALTH_CHECK_ACTION_NONCE_HEALTH.to_string())
577 );
578 assert!(!metadata.contains_key(HEALTH_CHECK_NONCE_HINT_KEY));
579 }
580
581 #[test]
582 fn test_relayer_health_check_nonce_health_with_hint() {
583 let job = RelayerHealthCheck::nonce_health_with_hint("relayer-1".to_string(), 274);
584
585 assert_eq!(job.relayer_id, "relayer-1");
586 let metadata = job.metadata.as_ref().unwrap();
587 assert_eq!(
588 metadata.get(HEALTH_CHECK_ACTION_KEY),
589 Some(&HEALTH_CHECK_ACTION_NONCE_HEALTH.to_string())
590 );
591 assert_eq!(
592 metadata.get(HEALTH_CHECK_NONCE_HINT_KEY),
593 Some(&"274".to_string())
594 );
595 }
596
597 #[test]
598 fn test_relayer_health_check_correct_field_values() {
599 let health_check_zero = RelayerHealthCheck::new("relayer-test-123".to_string());
601 assert_eq!(health_check_zero.relayer_id, "relayer-test-123");
602 assert_eq!(health_check_zero.retry_count, 0);
603
604 let health_check_custom =
606 RelayerHealthCheck::with_retry_count("relayer-abc".to_string(), 10);
607 assert_eq!(health_check_custom.relayer_id, "relayer-abc");
608 assert_eq!(health_check_custom.retry_count, 10);
609
610 let health_check_large =
612 RelayerHealthCheck::with_retry_count("relayer-xyz".to_string(), 999);
613 assert_eq!(health_check_large.relayer_id, "relayer-xyz");
614 assert_eq!(health_check_large.retry_count, 999);
615 }
616
617 #[test]
618 fn test_relayer_health_check_job_serialization() {
619 let health_check = RelayerHealthCheck::new("relayer-1".to_string());
620 let job = Job::new(JobType::RelayerHealthCheck, health_check);
621
622 let serialized = serde_json::to_string(&job).unwrap();
623 let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
624
625 assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
626 assert_eq!(deserialized.data.relayer_id, "relayer-1");
627 assert_eq!(deserialized.data.retry_count, 0);
628 }
629
630 #[test]
631 fn test_relayer_health_check_job_serialization_with_retry_count() {
632 let health_check = RelayerHealthCheck::with_retry_count("relayer-2".to_string(), 3);
633 let job = Job::new(JobType::RelayerHealthCheck, health_check.clone());
634
635 let serialized = serde_json::to_string(&job).unwrap();
636 let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
637
638 assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
639 assert_eq!(deserialized.data.relayer_id, health_check.relayer_id);
640 assert_eq!(deserialized.data.retry_count, health_check.retry_count);
641 assert_eq!(deserialized.data, health_check);
642 }
643
644 #[test]
645 fn test_relayer_health_check_equality_after_deserialization() {
646 let original_health_check =
647 RelayerHealthCheck::with_retry_count("relayer-test".to_string(), 7);
648 let job = Job::new(JobType::RelayerHealthCheck, original_health_check.clone());
649
650 let serialized = serde_json::to_string(&job).unwrap();
651 let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
652
653 assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
655
656 assert_eq!(deserialized.data, original_health_check);
658 assert_eq!(
659 deserialized.data.relayer_id,
660 original_health_check.relayer_id
661 );
662 assert_eq!(
663 deserialized.data.retry_count,
664 original_health_check.retry_count
665 );
666 }
667
668 #[test]
669 fn test_relayer_health_check_with_metadata() {
670 let mut metadata = HashMap::new();
671 metadata.insert(
672 "health_check_action".to_string(),
673 "nonce_health".to_string(),
674 );
675
676 let health_check =
677 RelayerHealthCheck::new("relayer-1".to_string()).with_metadata(metadata.clone());
678
679 assert_eq!(health_check.relayer_id, "relayer-1");
680 assert_eq!(health_check.retry_count, 0);
681 assert!(health_check.metadata.is_some());
682 assert_eq!(
683 health_check
684 .metadata
685 .as_ref()
686 .unwrap()
687 .get("health_check_action"),
688 Some(&"nonce_health".to_string())
689 );
690 assert_eq!(health_check.metadata.unwrap(), metadata);
691 }
692
693 #[test]
694 fn test_relayer_health_check_metadata_serialization() {
695 let mut metadata = HashMap::new();
696 metadata.insert(
697 "health_check_action".to_string(),
698 "nonce_health".to_string(),
699 );
700
701 let original = RelayerHealthCheck::with_retry_count("relayer-2".to_string(), 2)
702 .with_metadata(metadata.clone());
703
704 let serialized = serde_json::to_string(&original).unwrap();
705 let deserialized: RelayerHealthCheck = serde_json::from_str(&serialized).unwrap();
706
707 assert_eq!(deserialized.relayer_id, original.relayer_id);
708 assert_eq!(deserialized.retry_count, original.retry_count);
709 assert_eq!(deserialized.metadata, original.metadata);
710 assert_eq!(
711 deserialized
712 .metadata
713 .as_ref()
714 .unwrap()
715 .get("health_check_action"),
716 Some(&"nonce_health".to_string())
717 );
718 }
719
720 #[test]
721 fn test_relayer_health_check_backward_compatibility() {
722 let old_json = r#"{
724 "relayer_id": "relayer-legacy",
725 "retry_count": 3
726 }"#;
727
728 let deserialized: RelayerHealthCheck = serde_json::from_str(old_json).unwrap();
729
730 assert_eq!(deserialized.relayer_id, "relayer-legacy");
731 assert_eq!(deserialized.retry_count, 3);
732 assert!(deserialized.metadata.is_none());
733 }
734}