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::models::WebhookNotification;
8use chrono::Utc;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use strum::Display;
12use uuid::Uuid;
13
14// Common message structure
15#[derive(Debug, Serialize, Deserialize, Clone)]
16pub struct Job<T> {
17    pub message_id: String,
18    pub version: String,
19    pub timestamp: String,
20    pub job_type: JobType,
21    pub data: T,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub request_id: Option<String>,
24}
25
26impl<T> Job<T> {
27    pub fn new(job_type: JobType, data: T) -> Self {
28        Self {
29            message_id: Uuid::new_v4().to_string(),
30            version: "1.0".to_string(),
31            timestamp: Utc::now().timestamp().to_string(),
32            job_type,
33            data,
34            request_id: None,
35        }
36    }
37    pub fn with_request_id(mut self, id: Option<String>) -> Self {
38        self.request_id = id;
39        self
40    }
41}
42
43// Enum to represent different message types
44#[derive(Debug, Serialize, Deserialize, Display, Clone)]
45#[serde(tag = "type", rename_all = "snake_case")]
46pub enum JobType {
47    TransactionRequest,
48    TransactionSend,
49    TransactionStatusCheck,
50    NotificationSend,
51    SolanaTokenSwapRequest,
52    RelayerHealthCheck,
53}
54
55// Example message data for transaction request
56#[derive(Debug, Serialize, Deserialize, Clone)]
57pub struct TransactionRequest {
58    pub transaction_id: String,
59    pub relayer_id: String,
60    pub metadata: Option<HashMap<String, String>>,
61}
62
63impl TransactionRequest {
64    pub fn new(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
65        Self {
66            transaction_id: transaction_id.into(),
67            relayer_id: relayer_id.into(),
68            metadata: None,
69        }
70    }
71
72    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
73        self.metadata = Some(metadata);
74        self
75    }
76}
77
78#[derive(Debug, Serialize, Deserialize, Clone)]
79pub enum TransactionCommand {
80    Submit,
81    Cancel { reason: String },
82    Resubmit,
83    Resend,
84}
85
86// Example message data for order creation
87#[derive(Debug, Serialize, Deserialize, Clone)]
88pub struct TransactionSend {
89    pub transaction_id: String,
90    pub relayer_id: String,
91    pub command: TransactionCommand,
92    pub metadata: Option<HashMap<String, String>>,
93}
94
95impl TransactionSend {
96    pub fn submit(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
97        Self {
98            transaction_id: transaction_id.into(),
99            relayer_id: relayer_id.into(),
100            command: TransactionCommand::Submit,
101            metadata: None,
102        }
103    }
104
105    pub fn cancel(
106        transaction_id: impl Into<String>,
107        relayer_id: impl Into<String>,
108        reason: impl Into<String>,
109    ) -> Self {
110        Self {
111            transaction_id: transaction_id.into(),
112            relayer_id: relayer_id.into(),
113            command: TransactionCommand::Cancel {
114                reason: reason.into(),
115            },
116            metadata: None,
117        }
118    }
119
120    pub fn resubmit(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
121        Self {
122            transaction_id: transaction_id.into(),
123            relayer_id: relayer_id.into(),
124            command: TransactionCommand::Resubmit,
125            metadata: None,
126        }
127    }
128
129    pub fn resend(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::Resend,
134            metadata: None,
135        }
136    }
137
138    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
139        self.metadata = Some(metadata);
140        self
141    }
142}
143
144// Struct for individual order item
145#[derive(Debug, Serialize, Deserialize, Clone)]
146pub struct TransactionStatusCheck {
147    pub transaction_id: String,
148    pub relayer_id: String,
149    pub metadata: Option<HashMap<String, String>>,
150}
151
152impl TransactionStatusCheck {
153    pub fn new(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
154        Self {
155            transaction_id: transaction_id.into(),
156            relayer_id: relayer_id.into(),
157            metadata: None,
158        }
159    }
160
161    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
162        self.metadata = Some(metadata);
163        self
164    }
165}
166
167#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
168pub struct NotificationSend {
169    pub notification_id: String,
170    pub notification: WebhookNotification,
171}
172
173impl NotificationSend {
174    pub fn new(notification_id: String, notification: WebhookNotification) -> Self {
175        Self {
176            notification_id,
177            notification,
178        }
179    }
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
183pub struct SolanaTokenSwapRequest {
184    pub relayer_id: String,
185}
186
187impl SolanaTokenSwapRequest {
188    pub fn new(relayer_id: String) -> Self {
189        Self { relayer_id }
190    }
191}
192
193#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
194pub struct RelayerHealthCheck {
195    pub relayer_id: String,
196    pub retry_count: u32,
197}
198
199impl RelayerHealthCheck {
200    pub fn new(relayer_id: String) -> Self {
201        Self {
202            relayer_id,
203            retry_count: 0,
204        }
205    }
206
207    pub fn with_retry_count(relayer_id: String, retry_count: u32) -> Self {
208        Self {
209            relayer_id,
210            retry_count,
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use std::collections::HashMap;
218    use std::str::FromStr;
219
220    use crate::models::{
221        evm::Speed, EvmTransactionDataSignature, EvmTransactionResponse, TransactionResponse,
222        TransactionStatus, WebhookNotification, WebhookPayload, U256,
223    };
224
225    use super::*;
226
227    #[test]
228    fn test_job_creation() {
229        let job_data = TransactionRequest::new("tx123", "relayer-1");
230        let job = Job::new(JobType::TransactionRequest, job_data.clone());
231
232        assert_eq!(job.job_type.to_string(), "TransactionRequest");
233        assert_eq!(job.version, "1.0");
234        assert_eq!(job.data.transaction_id, "tx123");
235        assert_eq!(job.data.relayer_id, "relayer-1");
236        assert!(job.data.metadata.is_none());
237    }
238
239    #[test]
240    fn test_transaction_request_with_metadata() {
241        let mut metadata = HashMap::new();
242        metadata.insert("chain_id".to_string(), "1".to_string());
243        metadata.insert("gas_price".to_string(), "20000000000".to_string());
244
245        let tx_request =
246            TransactionRequest::new("tx123", "relayer-1").with_metadata(metadata.clone());
247
248        assert_eq!(tx_request.transaction_id, "tx123");
249        assert_eq!(tx_request.relayer_id, "relayer-1");
250        assert!(tx_request.metadata.is_some());
251        assert_eq!(tx_request.metadata.unwrap(), metadata);
252    }
253
254    #[test]
255    fn test_transaction_send_methods() {
256        // Test submit
257        let tx_submit = TransactionSend::submit("tx123", "relayer-1");
258        assert_eq!(tx_submit.transaction_id, "tx123");
259        assert_eq!(tx_submit.relayer_id, "relayer-1");
260        matches!(tx_submit.command, TransactionCommand::Submit);
261
262        // Test cancel
263        let tx_cancel = TransactionSend::cancel("tx123", "relayer-1", "user requested");
264        matches!(tx_cancel.command, TransactionCommand::Cancel { reason } if reason == "user requested");
265
266        // Test resubmit
267        let tx_resubmit = TransactionSend::resubmit("tx123", "relayer-1");
268        matches!(tx_resubmit.command, TransactionCommand::Resubmit);
269
270        // Test resend
271        let tx_resend = TransactionSend::resend("tx123", "relayer-1");
272        matches!(tx_resend.command, TransactionCommand::Resend);
273
274        // Test with_metadata
275        let mut metadata = HashMap::new();
276        metadata.insert("nonce".to_string(), "5".to_string());
277
278        let tx_with_metadata =
279            TransactionSend::submit("tx123", "relayer-1").with_metadata(metadata.clone());
280
281        assert!(tx_with_metadata.metadata.is_some());
282        assert_eq!(tx_with_metadata.metadata.unwrap(), metadata);
283    }
284
285    #[test]
286    fn test_transaction_status_check() {
287        let tx_status = TransactionStatusCheck::new("tx123", "relayer-1");
288        assert_eq!(tx_status.transaction_id, "tx123");
289        assert_eq!(tx_status.relayer_id, "relayer-1");
290        assert!(tx_status.metadata.is_none());
291
292        let mut metadata = HashMap::new();
293        metadata.insert("retries".to_string(), "3".to_string());
294
295        let tx_status_with_metadata =
296            TransactionStatusCheck::new("tx123", "relayer-1").with_metadata(metadata.clone());
297
298        assert!(tx_status_with_metadata.metadata.is_some());
299        assert_eq!(tx_status_with_metadata.metadata.unwrap(), metadata);
300    }
301
302    #[test]
303    fn test_job_serialization() {
304        let tx_request = TransactionRequest::new("tx123", "relayer-1");
305        let job = Job::new(JobType::TransactionRequest, tx_request);
306
307        let serialized = serde_json::to_string(&job).unwrap();
308        let deserialized: Job<TransactionRequest> = serde_json::from_str(&serialized).unwrap();
309
310        assert_eq!(deserialized.job_type.to_string(), "TransactionRequest");
311        assert_eq!(deserialized.data.transaction_id, "tx123");
312        assert_eq!(deserialized.data.relayer_id, "relayer-1");
313    }
314
315    #[test]
316    fn test_notification_send_serialization() {
317        let payload = WebhookPayload::Transaction(TransactionResponse::Evm(Box::new(
318            EvmTransactionResponse {
319                id: "tx123".to_string(),
320                hash: Some("0x123".to_string()),
321                status: TransactionStatus::Confirmed,
322                status_reason: None,
323                created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
324                sent_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
325                confirmed_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
326                gas_price: Some(1000000000),
327                gas_limit: Some(21000),
328                nonce: Some(1),
329                value: U256::from_str("1000000000000000000").unwrap(),
330                from: "0xabc".to_string(),
331                to: Some("0xdef".to_string()),
332                relayer_id: "relayer-1".to_string(),
333                data: Some("0x123".to_string()),
334                max_fee_per_gas: Some(1000000000),
335                max_priority_fee_per_gas: Some(1000000000),
336                signature: Some(EvmTransactionDataSignature {
337                    r: "0x123".to_string(),
338                    s: "0x123".to_string(),
339                    v: 1,
340                    sig: "0x123".to_string(),
341                }),
342                speed: Some(Speed::Fast),
343            },
344        )));
345
346        let notification = WebhookNotification::new("transaction".to_string(), payload);
347        let notification_send =
348            NotificationSend::new("notification-test".to_string(), notification);
349
350        let serialized = serde_json::to_string(&notification_send).unwrap();
351
352        match serde_json::from_str::<NotificationSend>(&serialized) {
353            Ok(deserialized) => {
354                assert_eq!(notification_send, deserialized);
355            }
356            Err(e) => {
357                panic!("Deserialization error: {}", e);
358            }
359        }
360    }
361
362    #[test]
363    fn test_notification_send_serialization_none_values() {
364        let payload = WebhookPayload::Transaction(TransactionResponse::Evm(Box::new(
365            EvmTransactionResponse {
366                id: "tx123".to_string(),
367                hash: None,
368                status: TransactionStatus::Confirmed,
369                status_reason: None,
370                created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
371                sent_at: None,
372                confirmed_at: None,
373                gas_price: None,
374                gas_limit: Some(21000),
375                nonce: None,
376                value: U256::from_str("1000000000000000000").unwrap(),
377                from: "0xabc".to_string(),
378                to: None,
379                relayer_id: "relayer-1".to_string(),
380                data: None,
381                max_fee_per_gas: None,
382                max_priority_fee_per_gas: None,
383                signature: None,
384                speed: None,
385            },
386        )));
387
388        let notification = WebhookNotification::new("transaction".to_string(), payload);
389        let notification_send =
390            NotificationSend::new("notification-test".to_string(), notification);
391
392        let serialized = serde_json::to_string(&notification_send).unwrap();
393
394        match serde_json::from_str::<NotificationSend>(&serialized) {
395            Ok(deserialized) => {
396                assert_eq!(notification_send, deserialized);
397            }
398            Err(e) => {
399                panic!("Deserialization error: {}", e);
400            }
401        }
402    }
403
404    #[test]
405    fn test_relayer_health_check_new() {
406        let health_check = RelayerHealthCheck::new("relayer-1".to_string());
407
408        assert_eq!(health_check.relayer_id, "relayer-1");
409        assert_eq!(health_check.retry_count, 0);
410    }
411
412    #[test]
413    fn test_relayer_health_check_with_retry_count() {
414        let health_check = RelayerHealthCheck::with_retry_count("relayer-1".to_string(), 5);
415
416        assert_eq!(health_check.relayer_id, "relayer-1");
417        assert_eq!(health_check.retry_count, 5);
418    }
419
420    #[test]
421    fn test_relayer_health_check_correct_field_values() {
422        // Test with zero retry count
423        let health_check_zero = RelayerHealthCheck::new("relayer-test-123".to_string());
424        assert_eq!(health_check_zero.relayer_id, "relayer-test-123");
425        assert_eq!(health_check_zero.retry_count, 0);
426
427        // Test with specific retry count
428        let health_check_custom =
429            RelayerHealthCheck::with_retry_count("relayer-abc".to_string(), 10);
430        assert_eq!(health_check_custom.relayer_id, "relayer-abc");
431        assert_eq!(health_check_custom.retry_count, 10);
432
433        // Test with large retry count
434        let health_check_large =
435            RelayerHealthCheck::with_retry_count("relayer-xyz".to_string(), 999);
436        assert_eq!(health_check_large.relayer_id, "relayer-xyz");
437        assert_eq!(health_check_large.retry_count, 999);
438    }
439
440    #[test]
441    fn test_relayer_health_check_job_serialization() {
442        let health_check = RelayerHealthCheck::new("relayer-1".to_string());
443        let job = Job::new(JobType::RelayerHealthCheck, health_check);
444
445        let serialized = serde_json::to_string(&job).unwrap();
446        let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
447
448        assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
449        assert_eq!(deserialized.data.relayer_id, "relayer-1");
450        assert_eq!(deserialized.data.retry_count, 0);
451    }
452
453    #[test]
454    fn test_relayer_health_check_job_serialization_with_retry_count() {
455        let health_check = RelayerHealthCheck::with_retry_count("relayer-2".to_string(), 3);
456        let job = Job::new(JobType::RelayerHealthCheck, health_check.clone());
457
458        let serialized = serde_json::to_string(&job).unwrap();
459        let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
460
461        assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
462        assert_eq!(deserialized.data.relayer_id, health_check.relayer_id);
463        assert_eq!(deserialized.data.retry_count, health_check.retry_count);
464        assert_eq!(deserialized.data, health_check);
465    }
466
467    #[test]
468    fn test_relayer_health_check_equality_after_deserialization() {
469        let original_health_check =
470            RelayerHealthCheck::with_retry_count("relayer-test".to_string(), 7);
471        let job = Job::new(JobType::RelayerHealthCheck, original_health_check.clone());
472
473        let serialized = serde_json::to_string(&job).unwrap();
474        let deserialized: Job<RelayerHealthCheck> = serde_json::from_str(&serialized).unwrap();
475
476        // Assert job type string
477        assert_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
478
479        // Assert data equality
480        assert_eq!(deserialized.data, original_health_check);
481        assert_eq!(
482            deserialized.data.relayer_id,
483            original_health_check.relayer_id
484        );
485        assert_eq!(
486            deserialized.data.retry_count,
487            original_health_check.retry_count
488        );
489    }
490}