openzeppelin_relayer/jobs/
job.rs

1//! Job processing module for handling asynchronous tasks.
2//!
3//! Provides generic job structure for different types of operations:
4//! - Transaction processing
5//! - Status monitoring
6//! - Notifications
7use 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// Common message structure
18#[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    /// Unix epoch (seconds) when this job is intended to become available for
28    /// processing. Set by the producer when `scheduled_on` is provided; absent
29    /// for immediate jobs. Consumers use this to compute queue pickup latency
30    /// that excludes intentional scheduling delay.
31    #[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// Enum to represent different message types
58#[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// Example message data for transaction request
70#[derive(Debug, Serialize, Deserialize, Clone)]
71pub struct TransactionRequest {
72    pub transaction_id: String,
73    pub relayer_id: String,
74    /// Network type for this transaction request.
75    /// Used by SQS backend to choose the FIFO message group strategy:
76    /// EVM uses relayer_id (nonce ordering), others use transaction_id (parallelism).
77    /// Optional for backward compatibility with older queued messages.
78    #[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// Example message data for order creation
113#[derive(Debug, Serialize, Deserialize, Clone)]
114pub struct TransactionSend {
115    pub transaction_id: String,
116    pub relayer_id: String,
117    pub command: TransactionCommand,
118    /// Network type for this transaction submission.
119    /// Used by SQS backend to choose the FIFO message group strategy:
120    /// EVM uses relayer_id (nonce ordering), others use transaction_id (parallelism).
121    /// Optional for backward compatibility with older queued messages.
122    #[serde(default)]
123    pub network_type: Option<NetworkType>,
124    pub metadata: Option<HashMap<String, String>>,
125}
126
127impl TransactionSend {
128    // Submit a transaction to the relayer
129    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    // Cancel a transaction
140    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    // Resubmit a transaction
157    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    // Resend a transaction
168    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    // Set the network type for this transaction submission
179    pub fn with_network_type(mut self, network_type: NetworkType) -> Self {
180        self.network_type = Some(network_type);
181        self
182    }
183
184    // Set the metadata for this transaction submission
185    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
186        self.metadata = Some(metadata);
187        self
188    }
189}
190
191// Struct for individual order item
192#[derive(Debug, Serialize, Deserialize, Clone)]
193pub struct TransactionStatusCheck {
194    pub transaction_id: String,
195    pub relayer_id: String,
196    /// Network type for this transaction status check.
197    /// Optional for backward compatibility with older queued messages.
198    #[serde(default)]
199    pub network_type: Option<NetworkType>,
200    pub metadata: Option<HashMap<String, String>>,
201}
202
203impl TransactionStatusCheck {
204    // Create a new transaction status check
205    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    // Set the metadata for this transaction status check
219    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    /// Optional metadata for targeted health actions (e.g., nonce health).
256    /// Backwards-compatible: old messages without this field deserialize as `None`.
257    #[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    /// Creates a nonce health check job for the given relayer.
284    /// Pre-populates metadata with the nonce health action key.
285    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    /// Creates a nonce health check job with a nonce hint.
295    /// The hint tells `resolve_nonce_gaps` to raise the counter to cover
296    /// this nonce, even if the counter was reset below it (e.g., after restart).
297    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        // Test submit
351        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        // Test cancel
357        let tx_cancel = TransactionSend::cancel("tx123", "relayer-1", "user requested");
358        matches!(tx_cancel.command, TransactionCommand::Cancel { reason } if reason == "user requested");
359
360        // Test resubmit
361        let tx_resubmit = TransactionSend::resubmit("tx123", "relayer-1");
362        matches!(tx_resubmit.command, TransactionCommand::Resubmit);
363
364        // Test resend
365        let tx_resend = TransactionSend::resend("tx123", "relayer-1");
366        matches!(tx_resend.command, TransactionCommand::Resend);
367
368        // Test with_metadata
369        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        // Simulate an old message without network_type field
401        let old_json = r#"{
402            "transaction_id": "tx456",
403            "relayer_id": "relayer-2",
404            "metadata": null
405        }"#;
406
407        // Should deserialize successfully with network_type defaulting to None
408        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        // New messages should include network_type
415        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(&notification_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(&notification_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        // Test with zero retry count
600        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        // Test with specific retry count
605        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        // Test with large retry count
611        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 job type string
654        assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
655
656        // Assert data equality
657        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        // Simulate an old message without the metadata field
723        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}