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}
23
24impl<T> Job<T> {
25    pub fn new(job_type: JobType, data: T) -> Self {
26        Self {
27            message_id: Uuid::new_v4().to_string(),
28            version: "1.0".to_string(),
29            timestamp: Utc::now().timestamp().to_string(),
30            job_type,
31            data,
32        }
33    }
34}
35
36// Enum to represent different message types
37#[derive(Debug, Serialize, Deserialize, Display, Clone)]
38#[serde(tag = "type", rename_all = "snake_case")]
39pub enum JobType {
40    TransactionRequest,
41    TransactionSend,
42    TransactionStatusCheck,
43    NotificationSend,
44    SolanaTokenSwapRequest,
45}
46
47// Example message data for transaction request
48#[derive(Debug, Serialize, Deserialize, Clone)]
49pub struct TransactionRequest {
50    pub transaction_id: String,
51    pub relayer_id: String,
52    pub metadata: Option<HashMap<String, String>>,
53}
54
55impl TransactionRequest {
56    pub fn new(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
57        Self {
58            transaction_id: transaction_id.into(),
59            relayer_id: relayer_id.into(),
60            metadata: None,
61        }
62    }
63
64    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
65        self.metadata = Some(metadata);
66        self
67    }
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone)]
71pub enum TransactionCommand {
72    Submit,
73    Cancel { reason: String },
74    Resubmit,
75    Resend,
76}
77
78// Example message data for order creation
79#[derive(Debug, Serialize, Deserialize, Clone)]
80pub struct TransactionSend {
81    pub transaction_id: String,
82    pub relayer_id: String,
83    pub command: TransactionCommand,
84    pub metadata: Option<HashMap<String, String>>,
85}
86
87impl TransactionSend {
88    pub fn submit(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
89        Self {
90            transaction_id: transaction_id.into(),
91            relayer_id: relayer_id.into(),
92            command: TransactionCommand::Submit,
93            metadata: None,
94        }
95    }
96
97    pub fn cancel(
98        transaction_id: impl Into<String>,
99        relayer_id: impl Into<String>,
100        reason: impl Into<String>,
101    ) -> Self {
102        Self {
103            transaction_id: transaction_id.into(),
104            relayer_id: relayer_id.into(),
105            command: TransactionCommand::Cancel {
106                reason: reason.into(),
107            },
108            metadata: None,
109        }
110    }
111
112    pub fn resubmit(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
113        Self {
114            transaction_id: transaction_id.into(),
115            relayer_id: relayer_id.into(),
116            command: TransactionCommand::Resubmit,
117            metadata: None,
118        }
119    }
120
121    pub fn resend(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
122        Self {
123            transaction_id: transaction_id.into(),
124            relayer_id: relayer_id.into(),
125            command: TransactionCommand::Resend,
126            metadata: None,
127        }
128    }
129
130    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
131        self.metadata = Some(metadata);
132        self
133    }
134}
135
136// Struct for individual order item
137#[derive(Debug, Serialize, Deserialize, Clone)]
138pub struct TransactionStatusCheck {
139    pub transaction_id: String,
140    pub relayer_id: String,
141    pub metadata: Option<HashMap<String, String>>,
142}
143
144impl TransactionStatusCheck {
145    pub fn new(transaction_id: impl Into<String>, relayer_id: impl Into<String>) -> Self {
146        Self {
147            transaction_id: transaction_id.into(),
148            relayer_id: relayer_id.into(),
149            metadata: None,
150        }
151    }
152
153    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
154        self.metadata = Some(metadata);
155        self
156    }
157}
158
159#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
160pub struct NotificationSend {
161    pub notification_id: String,
162    pub notification: WebhookNotification,
163}
164
165impl NotificationSend {
166    pub fn new(notification_id: String, notification: WebhookNotification) -> Self {
167        Self {
168            notification_id,
169            notification,
170        }
171    }
172}
173
174#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
175pub struct SolanaTokenSwapRequest {
176    pub relayer_id: String,
177}
178
179impl SolanaTokenSwapRequest {
180    pub fn new(relayer_id: String) -> Self {
181        Self { relayer_id }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use std::collections::HashMap;
188    use std::str::FromStr;
189
190    use crate::models::{
191        EvmTransactionResponse, TransactionResponse, TransactionStatus, WebhookNotification,
192        WebhookPayload, U256,
193    };
194
195    use super::*;
196
197    #[test]
198    fn test_job_creation() {
199        let job_data = TransactionRequest::new("tx123", "relayer-1");
200        let job = Job::new(JobType::TransactionRequest, job_data.clone());
201
202        assert_eq!(job.job_type.to_string(), "TransactionRequest");
203        assert_eq!(job.version, "1.0");
204        assert_eq!(job.data.transaction_id, "tx123");
205        assert_eq!(job.data.relayer_id, "relayer-1");
206        assert!(job.data.metadata.is_none());
207    }
208
209    #[test]
210    fn test_transaction_request_with_metadata() {
211        let mut metadata = HashMap::new();
212        metadata.insert("chain_id".to_string(), "1".to_string());
213        metadata.insert("gas_price".to_string(), "20000000000".to_string());
214
215        let tx_request =
216            TransactionRequest::new("tx123", "relayer-1").with_metadata(metadata.clone());
217
218        assert_eq!(tx_request.transaction_id, "tx123");
219        assert_eq!(tx_request.relayer_id, "relayer-1");
220        assert!(tx_request.metadata.is_some());
221        assert_eq!(tx_request.metadata.unwrap(), metadata);
222    }
223
224    #[test]
225    fn test_transaction_send_methods() {
226        // Test submit
227        let tx_submit = TransactionSend::submit("tx123", "relayer-1");
228        assert_eq!(tx_submit.transaction_id, "tx123");
229        assert_eq!(tx_submit.relayer_id, "relayer-1");
230        matches!(tx_submit.command, TransactionCommand::Submit);
231
232        // Test cancel
233        let tx_cancel = TransactionSend::cancel("tx123", "relayer-1", "user requested");
234        matches!(tx_cancel.command, TransactionCommand::Cancel { reason } if reason == "user requested");
235
236        // Test resubmit
237        let tx_resubmit = TransactionSend::resubmit("tx123", "relayer-1");
238        matches!(tx_resubmit.command, TransactionCommand::Resubmit);
239
240        // Test resend
241        let tx_resend = TransactionSend::resend("tx123", "relayer-1");
242        matches!(tx_resend.command, TransactionCommand::Resend);
243
244        // Test with_metadata
245        let mut metadata = HashMap::new();
246        metadata.insert("nonce".to_string(), "5".to_string());
247
248        let tx_with_metadata =
249            TransactionSend::submit("tx123", "relayer-1").with_metadata(metadata.clone());
250
251        assert!(tx_with_metadata.metadata.is_some());
252        assert_eq!(tx_with_metadata.metadata.unwrap(), metadata);
253    }
254
255    #[test]
256    fn test_transaction_status_check() {
257        let tx_status = TransactionStatusCheck::new("tx123", "relayer-1");
258        assert_eq!(tx_status.transaction_id, "tx123");
259        assert_eq!(tx_status.relayer_id, "relayer-1");
260        assert!(tx_status.metadata.is_none());
261
262        let mut metadata = HashMap::new();
263        metadata.insert("retries".to_string(), "3".to_string());
264
265        let tx_status_with_metadata =
266            TransactionStatusCheck::new("tx123", "relayer-1").with_metadata(metadata.clone());
267
268        assert!(tx_status_with_metadata.metadata.is_some());
269        assert_eq!(tx_status_with_metadata.metadata.unwrap(), metadata);
270    }
271
272    #[test]
273    fn test_job_serialization() {
274        let tx_request = TransactionRequest::new("tx123", "relayer-1");
275        let job = Job::new(JobType::TransactionRequest, tx_request);
276
277        let serialized = serde_json::to_string(&job).unwrap();
278        let deserialized: Job<TransactionRequest> = serde_json::from_str(&serialized).unwrap();
279
280        assert_eq!(deserialized.job_type.to_string(), "TransactionRequest");
281        assert_eq!(deserialized.data.transaction_id, "tx123");
282        assert_eq!(deserialized.data.relayer_id, "relayer-1");
283    }
284
285    #[test]
286    fn test_notification_send_serialization() {
287        let payload =
288            WebhookPayload::Transaction(TransactionResponse::Evm(EvmTransactionResponse {
289                id: "tx123".to_string(),
290                hash: Some("0x123".to_string()),
291                status: TransactionStatus::Confirmed,
292                status_reason: None,
293                created_at: "2025-01-27T15:31:10.777083+00:00".to_string(),
294                sent_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
295                confirmed_at: Some("2025-01-27T15:31:10.777083+00:00".to_string()),
296                gas_price: Some(1000000000),
297                gas_limit: 21000,
298                nonce: Some(1),
299                value: U256::from_str("1000000000000000000").unwrap(),
300                from: "0xabc".to_string(),
301                to: Some("0xdef".to_string()),
302                relayer_id: "relayer-1".to_string(),
303            }));
304
305        let notification = WebhookNotification::new("transaction".to_string(), payload);
306        let notification_send =
307            NotificationSend::new("notification-test".to_string(), notification);
308
309        let serialized = serde_json::to_string(&notification_send).unwrap();
310        match serde_json::from_str::<NotificationSend>(&serialized) {
311            Ok(deserialized) => {
312                assert_eq!(notification_send, deserialized);
313            }
314            Err(e) => {
315                eprintln!("Failed to deserialize NotificationSend: {}", e);
316                panic!("Deserialization error: {}", e);
317            }
318        }
319    }
320}