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