1use crate::{
2 domain::SwapResult,
3 jobs::NotificationSend,
4 models::{
5 RelayerRepoModel, RelayerResponse, SolanaSignAndSendTransactionResult,
6 SolanaSignTransactionResult, SolanaTransferTransactionResult, TransactionRepoModel,
7 TransactionResponse,
8 },
9};
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
15pub struct WebhookNotification {
16 pub id: String,
17 pub event: String,
18 pub payload: WebhookPayload,
19 pub timestamp: String,
20}
21
22impl WebhookNotification {
23 pub fn new(event: String, payload: WebhookPayload) -> Self {
24 Self {
25 id: Uuid::new_v4().to_string(),
26 event,
27 payload,
28 timestamp: Utc::now().to_rfc3339(),
29 }
30 }
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
34pub struct TransactionFailurePayload {
35 pub transaction: TransactionResponse,
36 pub failure_reason: String,
37}
38
39#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40pub struct RelayerDisabledPayload {
41 pub relayer: RelayerResponse,
42 pub disable_reason: String,
43}
44
45#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
46pub struct RelayerEnabledPayload {
47 pub relayer: RelayerResponse,
48 pub retry_count: u32,
49}
50#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
51pub struct SolanaDexPayload {
52 pub swap_results: Vec<SwapResult>,
53}
54
55#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
56pub struct StellarDexPayload {
57 pub swap_results: Vec<SwapResult>,
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
61#[serde(rename_all = "lowercase")]
62#[serde(tag = "payload_type")]
63pub enum WebhookPayload {
64 Transaction(TransactionResponse),
65 #[serde(rename = "transaction_failure")]
66 TransactionFailure(TransactionFailurePayload),
67 #[serde(rename = "relayer_disabled")]
68 RelayerDisabled(Box<RelayerDisabledPayload>),
69 #[serde(rename = "relayer_enabled")]
70 RelayerEnabled(Box<RelayerEnabledPayload>),
71 #[serde(rename = "solana_rpc")]
72 SolanaRpc(SolanaWebhookRpcPayload),
73 #[serde(rename = "solana_dex")]
74 SolanaDex(SolanaDexPayload),
75 #[serde(rename = "stellar_dex")]
76 StellarDex(StellarDexPayload),
77}
78
79#[derive(Debug, Serialize, Deserialize, Clone)]
80pub struct WebhookResponse {
81 pub status: String,
82 pub message: Option<String>,
83}
84
85pub fn produce_transaction_update_notification_payload(
86 notification_id: &str,
87 transaction: &TransactionRepoModel,
88) -> NotificationSend {
89 let tx_payload: TransactionResponse = transaction.clone().into();
90 NotificationSend::new(
91 notification_id.to_string(),
92 WebhookNotification::new(
93 "transaction_update".to_string(),
94 WebhookPayload::Transaction(tx_payload),
95 ),
96 )
97}
98
99pub fn produce_relayer_disabled_payload(
100 notification_id: &str,
101 relayer: &RelayerRepoModel,
102 reason: &str,
103) -> NotificationSend {
104 let relayer_response: RelayerResponse = relayer.clone().into();
105 let payload = RelayerDisabledPayload {
106 relayer: relayer_response,
107 disable_reason: reason.to_string(),
108 };
109 NotificationSend::new(
110 notification_id.to_string(),
111 WebhookNotification::new(
112 "relayer_state_update".to_string(),
113 WebhookPayload::RelayerDisabled(Box::new(payload)),
114 ),
115 )
116}
117
118pub fn produce_relayer_enabled_payload(
119 notification_id: &str,
120 relayer: &RelayerRepoModel,
121 retry_count: u32,
122) -> NotificationSend {
123 let relayer_response: RelayerResponse = relayer.clone().into();
124 let payload = RelayerEnabledPayload {
125 relayer: relayer_response,
126 retry_count,
127 };
128 NotificationSend::new(
129 notification_id.to_string(),
130 WebhookNotification::new(
131 "relayer_state_update".to_string(),
132 WebhookPayload::RelayerEnabled(Box::new(payload)),
133 ),
134 )
135}
136
137#[allow(clippy::enum_variant_names)]
138#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
139#[serde(untagged)]
140pub enum SolanaWebhookRpcPayload {
141 SignAndSendTransaction(SolanaSignAndSendTransactionResult),
142 SignTransaction(SolanaSignTransactionResult),
143 TransferTransaction(SolanaTransferTransactionResult),
144}
145
146pub fn produce_solana_rpc_webhook_payload(
148 notification_id: &str,
149 event: String,
150 payload: SolanaWebhookRpcPayload,
151) -> NotificationSend {
152 NotificationSend::new(
153 notification_id.to_string(),
154 WebhookNotification::new(event, WebhookPayload::SolanaRpc(payload)),
155 )
156}
157
158pub fn produce_solana_dex_webhook_payload(
159 notification_id: &str,
160 event: String,
161 payload: SolanaDexPayload,
162) -> NotificationSend {
163 NotificationSend::new(
164 notification_id.to_string(),
165 WebhookNotification::new(event, WebhookPayload::SolanaDex(payload)),
166 )
167}
168
169pub fn produce_stellar_dex_webhook_payload(
170 notification_id: &str,
171 event: String,
172 payload: StellarDexPayload,
173) -> NotificationSend {
174 NotificationSend::new(
175 notification_id.to_string(),
176 WebhookNotification::new(event, WebhookPayload::StellarDex(payload)),
177 )
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::utils::mocks::mockutils::{create_mock_relayer, create_mock_transaction};
184
185 #[test]
186 fn test_webhook_notification_new() {
187 let payload = WebhookPayload::Transaction(create_mock_transaction().into());
188 let notification = WebhookNotification::new("test_event".to_string(), payload.clone());
189
190 assert!(!notification.id.is_empty(), "Should have an ID");
192 assert_eq!(notification.event, "test_event");
193 assert_eq!(notification.payload, payload);
194 assert!(
195 !notification.timestamp.is_empty(),
196 "Should have a timestamp"
197 );
198
199 assert!(
201 Uuid::parse_str(¬ification.id).is_ok(),
202 "ID should be a valid UUID"
203 );
204
205 assert!(
207 chrono::DateTime::parse_from_rfc3339(¬ification.timestamp).is_ok(),
208 "Timestamp should be valid RFC3339"
209 );
210 }
211
212 #[test]
213 fn test_webhook_notification_unique_ids() {
214 let payload = WebhookPayload::Transaction(create_mock_transaction().into());
215 let notification1 = WebhookNotification::new("event".to_string(), payload.clone());
216 let notification2 = WebhookNotification::new("event".to_string(), payload);
217
218 assert_ne!(
220 notification1.id, notification2.id,
221 "Each notification should have a unique UUID"
222 );
223 }
224
225 #[test]
226 fn test_produce_transaction_update_notification_payload() {
227 let transaction = create_mock_transaction();
228 let notification_id = "test-notification-id";
229
230 let result = produce_transaction_update_notification_payload(notification_id, &transaction);
231
232 assert_eq!(result.notification_id, notification_id);
234
235 assert_eq!(result.notification.event, "transaction_update");
237
238 match result.notification.payload {
240 WebhookPayload::Transaction(TransactionResponse::Evm(tx)) => {
241 assert_eq!(tx.id, transaction.id);
242 assert_eq!(tx.relayer_id, transaction.relayer_id);
243 }
244 _ => panic!("Expected Transaction(Evm) payload"),
245 }
246 }
247
248 #[test]
249 fn test_produce_relayer_disabled_payload() {
250 let relayer = create_mock_relayer("test-relayer".to_string(), false);
251 let notification_id = "test-notification-id";
252 let reason = "RPC endpoint validation failed";
253
254 let result = produce_relayer_disabled_payload(notification_id, &relayer, reason);
255
256 assert_eq!(result.notification_id, notification_id);
258
259 assert_eq!(result.notification.event, "relayer_state_update");
261
262 match result.notification.payload {
264 WebhookPayload::RelayerDisabled(payload) => {
265 assert_eq!(payload.relayer.id, relayer.id);
266 assert_eq!(payload.disable_reason, reason);
267 }
268 _ => panic!("Expected RelayerDisabled payload"),
269 }
270 }
271
272 #[test]
273 fn test_produce_relayer_disabled_payload_with_sensitive_info() {
274 let relayer = create_mock_relayer("test-relayer".to_string(), false);
275 let notification_id = "test-notification-id";
276 let reason = "RPC endpoint validation failed";
278
279 let result = produce_relayer_disabled_payload(notification_id, &relayer, reason);
280
281 match result.notification.payload {
282 WebhookPayload::RelayerDisabled(payload) => {
283 assert!(!payload.disable_reason.contains("http://"));
285 assert!(!payload.disable_reason.contains("https://"));
286 assert_eq!(payload.disable_reason, reason);
287 }
288 _ => panic!("Expected RelayerDisabled payload"),
289 }
290 }
291
292 #[test]
293 fn test_produce_relayer_enabled_payload() {
294 let relayer = create_mock_relayer("test-relayer".to_string(), true);
295 let notification_id = "test-notification-id";
296 let retry_count = 5;
297
298 let result = produce_relayer_enabled_payload(notification_id, &relayer, retry_count);
299
300 assert_eq!(result.notification_id, notification_id);
302
303 assert_eq!(result.notification.event, "relayer_state_update");
305
306 match result.notification.payload {
308 WebhookPayload::RelayerEnabled(payload) => {
309 assert_eq!(payload.relayer.id, relayer.id);
310 assert_eq!(payload.retry_count, retry_count);
311 }
312 _ => panic!("Expected RelayerEnabled payload"),
313 }
314 }
315
316 #[test]
317 fn test_produce_relayer_enabled_payload_with_zero_retries() {
318 let relayer = create_mock_relayer("test-relayer".to_string(), true);
319 let notification_id = "test-notification-id";
320
321 let result = produce_relayer_enabled_payload(notification_id, &relayer, 0);
322
323 match result.notification.payload {
324 WebhookPayload::RelayerEnabled(payload) => {
325 assert_eq!(payload.retry_count, 0);
326 }
327 _ => panic!("Expected RelayerEnabled payload"),
328 }
329 }
330
331 #[test]
332 fn test_produce_solana_rpc_webhook_payload() {
333 use crate::models::EncodedSerializedTransaction;
334
335 let notification_id = "test-notification-id";
336 let event = "solana_sign_transaction".to_string();
337 let solana_payload =
338 SolanaWebhookRpcPayload::SignTransaction(SolanaSignTransactionResult {
339 transaction: EncodedSerializedTransaction::new("test-transaction".to_string()),
340 signature: "test-signature".to_string(),
341 });
342
343 let result =
344 produce_solana_rpc_webhook_payload(notification_id, event.clone(), solana_payload);
345
346 assert_eq!(result.notification_id, notification_id);
348
349 assert_eq!(result.notification.event, event);
351
352 match result.notification.payload {
354 WebhookPayload::SolanaRpc(SolanaWebhookRpcPayload::SignTransaction(sig)) => {
355 assert_eq!(sig.signature, "test-signature");
356 }
357 _ => panic!("Expected SolanaRpc SignTransaction payload"),
358 }
359 }
360
361 #[test]
362 fn test_produce_solana_dex_webhook_payload() {
363 let notification_id = "test-notification-id";
364 let event = "solana_swap_completed".to_string();
365 let swap_results = vec![];
366 let dex_payload = SolanaDexPayload { swap_results };
367
368 let result =
369 produce_solana_dex_webhook_payload(notification_id, event.clone(), dex_payload.clone());
370
371 assert_eq!(result.notification_id, notification_id);
373
374 assert_eq!(result.notification.event, event);
376
377 match result.notification.payload {
379 WebhookPayload::SolanaDex(payload) => {
380 assert_eq!(payload.swap_results.len(), 0);
381 }
382 _ => panic!("Expected SolanaDex payload"),
383 }
384 }
385
386 #[test]
387 fn test_produce_stellar_dex_webhook_payload() {
388 let notification_id = "test-notification-id";
389 let event = "stellar_dex_queued".to_string();
390 let swap_results = vec![];
391 let dex_payload = StellarDexPayload { swap_results };
392
393 let result = produce_stellar_dex_webhook_payload(
394 notification_id,
395 event.clone(),
396 dex_payload.clone(),
397 );
398
399 assert_eq!(result.notification_id, notification_id);
401
402 assert_eq!(result.notification.event, event);
404
405 match result.notification.payload {
407 WebhookPayload::StellarDex(payload) => {
408 assert_eq!(payload.swap_results.len(), 0);
409 }
410 _ => panic!("Expected StellarDex payload"),
411 }
412 }
413
414 #[test]
415 fn test_webhook_payload_serialization_transaction() {
416 let transaction = create_mock_transaction();
417 let payload = WebhookPayload::Transaction(transaction.into());
418
419 let serialized = serde_json::to_value(&payload).unwrap();
420
421 assert_eq!(serialized["payload_type"], "transaction");
423 }
424
425 #[test]
426 fn test_webhook_payload_serialization_relayer_disabled() {
427 let relayer = create_mock_relayer("test".to_string(), false);
428 let payload = WebhookPayload::RelayerDisabled(Box::new(RelayerDisabledPayload {
429 relayer: relayer.into(),
430 disable_reason: "Test reason".to_string(),
431 }));
432
433 let serialized = serde_json::to_value(&payload).unwrap();
434
435 assert_eq!(serialized["payload_type"], "relayer_disabled");
437 assert!(serialized["disable_reason"].is_string());
438 }
439
440 #[test]
441 fn test_webhook_payload_serialization_relayer_enabled() {
442 let relayer = create_mock_relayer("test".to_string(), false);
443 let payload = WebhookPayload::RelayerEnabled(Box::new(RelayerEnabledPayload {
444 relayer: relayer.into(),
445 retry_count: 3,
446 }));
447
448 let serialized = serde_json::to_value(&payload).unwrap();
449
450 assert_eq!(serialized["payload_type"], "relayer_enabled");
452 assert_eq!(serialized["retry_count"], 3);
453 }
454
455 #[test]
456 fn test_notification_send_structure() {
457 let transaction = create_mock_transaction();
458 let notification_id = "test-notification-id";
459
460 let notification_send =
461 produce_transaction_update_notification_payload(notification_id, &transaction);
462
463 let serialized = serde_json::to_string(¬ification_send);
465 assert!(
466 serialized.is_ok(),
467 "NotificationSend should be serializable"
468 );
469
470 let deserialized: Result<NotificationSend, _> = serde_json::from_str(&serialized.unwrap());
472 assert!(
473 deserialized.is_ok(),
474 "NotificationSend should be deserializable"
475 );
476 }
477
478 #[test]
479 fn test_relayer_disabled_and_enabled_use_same_event() {
480 let relayer = create_mock_relayer("test".to_string(), false);
481
482 let disabled_notification =
483 produce_relayer_disabled_payload("notif-id", &relayer, "reason");
484 let enabled_notification = produce_relayer_enabled_payload("notif-id", &relayer, 1);
485
486 assert_eq!(
488 disabled_notification.notification.event,
489 enabled_notification.notification.event
490 );
491 assert_eq!(
492 disabled_notification.notification.event,
493 "relayer_state_update"
494 );
495 }
496}