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}
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// Enum to represent different message types
47#[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// Example message data for transaction request
59#[derive(Debug, Serialize, Deserialize, Clone)]
60pub struct TransactionRequest {
61    pub transaction_id: String,
62    pub relayer_id: String,
63    /// Network type for this transaction request.
64    /// Used by SQS backend to choose the FIFO message group strategy:
65    /// EVM uses relayer_id (nonce ordering), others use transaction_id (parallelism).
66    /// Optional for backward compatibility with older queued messages.
67    #[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// Example message data for order creation
102#[derive(Debug, Serialize, Deserialize, Clone)]
103pub struct TransactionSend {
104    pub transaction_id: String,
105    pub relayer_id: String,
106    pub command: TransactionCommand,
107    /// Network type for this transaction submission.
108    /// Used by SQS backend to choose the FIFO message group strategy:
109    /// EVM uses relayer_id (nonce ordering), others use transaction_id (parallelism).
110    /// Optional for backward compatibility with older queued messages.
111    #[serde(default)]
112    pub network_type: Option<NetworkType>,
113    pub metadata: Option<HashMap<String, String>>,
114}
115
116impl TransactionSend {
117    // Submit a transaction to the relayer
118    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    // Cancel a transaction
129    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    // Resubmit a transaction
146    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    // Resend a transaction
157    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    // Set the network type for this transaction submission
168    pub fn with_network_type(mut self, network_type: NetworkType) -> Self {
169        self.network_type = Some(network_type);
170        self
171    }
172
173    // Set the metadata for this transaction submission
174    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
175        self.metadata = Some(metadata);
176        self
177    }
178}
179
180// Struct for individual order item
181#[derive(Debug, Serialize, Deserialize, Clone)]
182pub struct TransactionStatusCheck {
183    pub transaction_id: String,
184    pub relayer_id: String,
185    /// Network type for this transaction status check.
186    /// Optional for backward compatibility with older queued messages.
187    #[serde(default)]
188    pub network_type: Option<NetworkType>,
189    pub metadata: Option<HashMap<String, String>>,
190}
191
192impl TransactionStatusCheck {
193    // Create a new transaction status check
194    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    // Set the metadata for this transaction status check
208    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    /// Optional metadata for targeted health actions (e.g., nonce health).
245    /// Backwards-compatible: old messages without this field deserialize as `None`.
246    #[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    /// Creates a nonce health check job for the given relayer.
273    /// Pre-populates metadata with the nonce health action key.
274    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    /// Creates a nonce health check job with a nonce hint.
284    /// The hint tells `resolve_nonce_gaps` to raise the counter to cover
285    /// this nonce, even if the counter was reset below it (e.g., after restart).
286    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        // Test submit
340        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        // Test cancel
346        let tx_cancel = TransactionSend::cancel("tx123", "relayer-1", "user requested");
347        matches!(tx_cancel.command, TransactionCommand::Cancel { reason } if reason == "user requested");
348
349        // Test resubmit
350        let tx_resubmit = TransactionSend::resubmit("tx123", "relayer-1");
351        matches!(tx_resubmit.command, TransactionCommand::Resubmit);
352
353        // Test resend
354        let tx_resend = TransactionSend::resend("tx123", "relayer-1");
355        matches!(tx_resend.command, TransactionCommand::Resend);
356
357        // Test with_metadata
358        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        // Simulate an old message without network_type field
390        let old_json = r#"{
391            "transaction_id": "tx456",
392            "relayer_id": "relayer-2",
393            "metadata": null
394        }"#;
395
396        // Should deserialize successfully with network_type defaulting to None
397        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        // New messages should include network_type
404        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(&notification_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(&notification_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        // Test with zero retry count
558        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        // Test with specific retry count
563        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        // Test with large retry count
569        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 job type string
612        assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
613
614        // Assert data equality
615        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        // Simulate an old message without the metadata field
681        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}