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            },
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(&notification_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(&notification_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        // Test with zero retry count
598        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        // Test with specific retry count
603        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        // Test with large retry count
609        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 job type string
652        assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
653
654        // Assert data equality
655        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        // Simulate an old message without the metadata field
721        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}