1use crate::models::WebhookNotification;
8use chrono::Utc;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use strum::Display;
12use uuid::Uuid;
13
14#[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#[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#[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#[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#[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 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 let tx_cancel = TransactionSend::cancel("tx123", "relayer-1", "user requested");
264 matches!(tx_cancel.command, TransactionCommand::Cancel { reason } if reason == "user requested");
265
266 let tx_resubmit = TransactionSend::resubmit("tx123", "relayer-1");
268 matches!(tx_resubmit.command, TransactionCommand::Resubmit);
269
270 let tx_resend = TransactionSend::resend("tx123", "relayer-1");
272 matches!(tx_resend.command, TransactionCommand::Resend);
273
274 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(¬ification_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(¬ification_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 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 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 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_eq!(deserialized.job_type.to_string(), "RelayerHealthCheck");
478
479 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}