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 },
490 )));
491
492 let notification = WebhookNotification::new("transaction".to_string(), payload);
493 let notification_send =
494 NotificationSend::new("notification-test".to_string(), notification);
495
496 let serialized = serde_json::to_string(¬ification_send).unwrap();
497
498 match serde_json::from_str::<NotificationSend>(&serialized) {
499 Ok(deserialized) => {
500 assert_eq!(notification_send, deserialized);
501 }
502 Err(e) => {
503 panic!("Deserialization error: {e}");
504 }
505 }
506 }
507
508 #[test]
509 fn test_notification_send_serialization_none_values() {
510 let payload = WebhookPayload::Transaction(TransactionResponse::Evm(Box::new(
511 EvmTransactionResponse {
512 id: "tx123".to_string(),
513 hash: None,
514 status: TransactionStatus::Confirmed,
515 status_reason: None,
516 created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
517 sent_at: None,
518 confirmed_at: None,
519 gas_price: None,
520 gas_limit: Some(21000),
521 nonce: None,
522 value: U256::from_str("1000000000000000000").unwrap(),
523 from: "0xabc".to_string(),
524 to: None,
525 relayer_id: "relayer-1".to_string(),
526 data: None,
527 max_fee_per_gas: None,
528 max_priority_fee_per_gas: None,
529 signature: None,
530 speed: None,
531 },
532 )));
533
534 let notification = WebhookNotification::new("transaction".to_string(), payload);
535 let notification_send =
536 NotificationSend::new("notification-test".to_string(), notification);
537
538 let serialized = serde_json::to_string(¬ification_send).unwrap();
539
540 match serde_json::from_str::<NotificationSend>(&serialized) {
541 Ok(deserialized) => {
542 assert_eq!(notification_send, deserialized);
543 }
544 Err(e) => {
545 panic!("Deserialization error: {e}");
546 }
547 }
548 }
549
550 #[test]
551 fn test_relayer_health_check_new() {
552 let health_check = RelayerHealthCheck::new("relayer-1".to_string());
553
554 assert_eq!(health_check.relayer_id, "relayer-1");
555 assert_eq!(health_check.retry_count, 0);
556 }
557
558 #[test]
559 fn test_relayer_health_check_with_retry_count() {
560 let health_check = RelayerHealthCheck::with_retry_count("relayer-1".to_string(), 5);
561
562 assert_eq!(health_check.relayer_id, "relayer-1");
563 assert_eq!(health_check.retry_count, 5);
564 }
565
566 #[test]
567 fn test_relayer_health_check_nonce_health() {
568 let job = RelayerHealthCheck::nonce_health("relayer-1".to_string());
569
570 assert_eq!(job.relayer_id, "relayer-1");
571 let metadata = job.metadata.as_ref().unwrap();
572 assert_eq!(
573 metadata.get(HEALTH_CHECK_ACTION_KEY),
574 Some(&HEALTH_CHECK_ACTION_NONCE_HEALTH.to_string())
575 );
576 assert!(!metadata.contains_key(HEALTH_CHECK_NONCE_HINT_KEY));
577 }
578
579 #[test]
580 fn test_relayer_health_check_nonce_health_with_hint() {
581 let job = RelayerHealthCheck::nonce_health_with_hint("relayer-1".to_string(), 274);
582
583 assert_eq!(job.relayer_id, "relayer-1");
584 let metadata = job.metadata.as_ref().unwrap();
585 assert_eq!(
586 metadata.get(HEALTH_CHECK_ACTION_KEY),
587 Some(&HEALTH_CHECK_ACTION_NONCE_HEALTH.to_string())
588 );
589 assert_eq!(
590 metadata.get(HEALTH_CHECK_NONCE_HINT_KEY),
591 Some(&"274".to_string())
592 );
593 }
594
595 #[test]
596 fn test_relayer_health_check_correct_field_values() {
597 let health_check_zero = RelayerHealthCheck::new("relayer-test-123".to_string());
599 assert_eq!(health_check_zero.relayer_id, "relayer-test-123");
600 assert_eq!(health_check_zero.retry_count, 0);
601
602 let health_check_custom =
604 RelayerHealthCheck::with_retry_count("relayer-abc".to_string(), 10);
605 assert_eq!(health_check_custom.relayer_id, "relayer-abc");
606 assert_eq!(health_check_custom.retry_count, 10);
607
608 let health_check_large =
610 RelayerHealthCheck::with_retry_count("relayer-xyz".to_string(), 999);
611 assert_eq!(health_check_large.relayer_id, "relayer-xyz");
612 assert_eq!(health_check_large.retry_count, 999);
613 }
614
615 #[test]
616 fn test_relayer_health_check_job_serialization() {
617 let health_check = RelayerHealthCheck::new("relayer-1".to_string());
618 let job = Job::new(JobType::RelayerHealthCheck, health_check);
619
620 let serialized = serde_json::to_string(&job).unwrap();
621 let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
622
623 assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
624 assert_eq!(deserialized.data.relayer_id, "relayer-1");
625 assert_eq!(deserialized.data.retry_count, 0);
626 }
627
628 #[test]
629 fn test_relayer_health_check_job_serialization_with_retry_count() {
630 let health_check = RelayerHealthCheck::with_retry_count("relayer-2".to_string(), 3);
631 let job = Job::new(JobType::RelayerHealthCheck, health_check.clone());
632
633 let serialized = serde_json::to_string(&job).unwrap();
634 let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
635
636 assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
637 assert_eq!(deserialized.data.relayer_id, health_check.relayer_id);
638 assert_eq!(deserialized.data.retry_count, health_check.retry_count);
639 assert_eq!(deserialized.data, health_check);
640 }
641
642 #[test]
643 fn test_relayer_health_check_equality_after_deserialization() {
644 let original_health_check =
645 RelayerHealthCheck::with_retry_count("relayer-test".to_string(), 7);
646 let job = Job::new(JobType::RelayerHealthCheck, original_health_check.clone());
647
648 let serialized = serde_json::to_string(&job).unwrap();
649 let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
650
651 assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
653
654 assert_eq!(deserialized.data, original_health_check);
656 assert_eq!(
657 deserialized.data.relayer_id,
658 original_health_check.relayer_id
659 );
660 assert_eq!(
661 deserialized.data.retry_count,
662 original_health_check.retry_count
663 );
664 }
665
666 #[test]
667 fn test_relayer_health_check_with_metadata() {
668 let mut metadata = HashMap::new();
669 metadata.insert(
670 "health_check_action".to_string(),
671 "nonce_health".to_string(),
672 );
673
674 let health_check =
675 RelayerHealthCheck::new("relayer-1".to_string()).with_metadata(metadata.clone());
676
677 assert_eq!(health_check.relayer_id, "relayer-1");
678 assert_eq!(health_check.retry_count, 0);
679 assert!(health_check.metadata.is_some());
680 assert_eq!(
681 health_check
682 .metadata
683 .as_ref()
684 .unwrap()
685 .get("health_check_action"),
686 Some(&"nonce_health".to_string())
687 );
688 assert_eq!(health_check.metadata.unwrap(), metadata);
689 }
690
691 #[test]
692 fn test_relayer_health_check_metadata_serialization() {
693 let mut metadata = HashMap::new();
694 metadata.insert(
695 "health_check_action".to_string(),
696 "nonce_health".to_string(),
697 );
698
699 let original = RelayerHealthCheck::with_retry_count("relayer-2".to_string(), 2)
700 .with_metadata(metadata.clone());
701
702 let serialized = serde_json::to_string(&original).unwrap();
703 let deserialized: RelayerHealthCheck = serde_json::from_str(&serialized).unwrap();
704
705 assert_eq!(deserialized.relayer_id, original.relayer_id);
706 assert_eq!(deserialized.retry_count, original.retry_count);
707 assert_eq!(deserialized.metadata, original.metadata);
708 assert_eq!(
709 deserialized
710 .metadata
711 .as_ref()
712 .unwrap()
713 .get("health_check_action"),
714 Some(&"nonce_health".to_string())
715 );
716 }
717
718 #[test]
719 fn test_relayer_health_check_backward_compatibility() {
720 let old_json = r#"{
722 "relayer_id": "relayer-legacy",
723 "retry_count": 3
724 }"#;
725
726 let deserialized: RelayerHealthCheck = serde_json::from_str(old_json).unwrap();
727
728 assert_eq!(deserialized.relayer_id, "relayer-legacy");
729 assert_eq!(deserialized.retry_count, 3);
730 assert!(deserialized.metadata.is_none());
731 }
732}