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