1use regex::Regex;
6use serde_json::json;
7use std::collections::HashMap;
8
9use super::template_formatter;
10
11pub trait WebhookPayloadBuilder: Send + Sync {
13 fn build_payload(
25 &self,
26 title: &str,
27 body_template: &str,
28 variables: &HashMap<String, String>,
29 ) -> serde_json::Value;
30}
31
32pub fn format_template(template: &str, variables: &HashMap<String, String>) -> String {
34 template_formatter::format_template(template, variables)
35}
36
37pub struct SlackPayloadBuilder;
39
40impl WebhookPayloadBuilder for SlackPayloadBuilder {
41 fn build_payload(
42 &self,
43 title: &str,
44 body_template: &str,
45 variables: &HashMap<String, String>,
46 ) -> serde_json::Value {
47 let formatted_title = format_template(title, variables);
48 let formatted_message = format_template(body_template, variables);
49 let full_message = format!("*{}*\n\n{}", formatted_title, formatted_message);
50 json!({
51 "blocks": [
52 {
53 "type": "section",
54 "text": {
55 "type": "mrkdwn",
56 "text": full_message
57 }
58 }
59 ]
60 })
61 }
62}
63
64pub struct DiscordPayloadBuilder;
66
67impl WebhookPayloadBuilder for DiscordPayloadBuilder {
68 fn build_payload(
69 &self,
70 title: &str,
71 body_template: &str,
72 variables: &HashMap<String, String>,
73 ) -> serde_json::Value {
74 let formatted_title = format_template(title, variables);
75 let formatted_message = format_template(body_template, variables);
76 let full_message = format!("*{}*\n\n{}", formatted_title, formatted_message);
77 json!({
78 "content": full_message
79 })
80 }
81}
82
83pub struct TelegramPayloadBuilder {
85 pub chat_id: String,
86 pub disable_web_preview: bool,
87}
88
89impl TelegramPayloadBuilder {
90 fn escape_markdown_v2(text: &str) -> String {
93 const SPECIAL: &[char] = &[
94 '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.',
95 '!', '\\',
96 ];
97
98 let re =
99 Regex::new(r"(?s)```.*?```|`[^`]*`|\*[^*]*\*|_[^_]*_|~[^~]*~|\[([^\]]+)\]\(([^)]+)\)")
100 .unwrap();
101
102 let mut out = String::with_capacity(text.len());
103 let mut last = 0;
104
105 for caps in re.captures_iter(text) {
106 let mat = caps.get(0).unwrap();
107
108 for c in text[last..mat.start()].chars() {
109 if SPECIAL.contains(&c) {
110 out.push('\\');
111 }
112 out.push(c);
113 }
114
115 if let (Some(lbl), Some(url)) = (caps.get(1), caps.get(2)) {
116 let mut esc_label = String::with_capacity(lbl.as_str().len() * 2);
117 for c in lbl.as_str().chars() {
118 if SPECIAL.contains(&c) {
119 esc_label.push('\\');
120 }
121 esc_label.push(c);
122 }
123 let mut esc_url = String::with_capacity(url.as_str().len() * 2);
124 for c in url.as_str().chars() {
125 if SPECIAL.contains(&c) {
126 esc_url.push('\\');
127 }
128 esc_url.push(c);
129 }
130 out.push('[');
131 out.push_str(&esc_label);
132 out.push(']');
133 out.push('(');
134 out.push_str(&esc_url);
135 out.push(')');
136 } else {
137 out.push_str(mat.as_str());
138 }
139
140 last = mat.end();
141 }
142
143 for c in text[last..].chars() {
144 if SPECIAL.contains(&c) {
145 out.push('\\');
146 }
147 out.push(c);
148 }
149
150 out
151 }
152}
153
154impl WebhookPayloadBuilder for TelegramPayloadBuilder {
155 fn build_payload(
156 &self,
157 title: &str,
158 body_template: &str,
159 variables: &HashMap<String, String>,
160 ) -> serde_json::Value {
161 let formatted_title = format_template(title, variables);
163 let formatted_message = format_template(body_template, variables);
164
165 let escaped_title = Self::escape_markdown_v2(&formatted_title);
167 let escaped_message = Self::escape_markdown_v2(&formatted_message);
168
169 let full_message = format!("*{}* \n\n{}", escaped_title, escaped_message);
170 json!({
171 "chat_id": self.chat_id,
172 "text": full_message,
173 "parse_mode": "MarkdownV2",
174 "disable_web_page_preview": self.disable_web_preview
175 })
176 }
177}
178
179pub struct GenericWebhookPayloadBuilder;
181
182impl WebhookPayloadBuilder for GenericWebhookPayloadBuilder {
183 fn build_payload(
184 &self,
185 title: &str,
186 body_template: &str,
187 variables: &HashMap<String, String>,
188 ) -> serde_json::Value {
189 let formatted_title = format_template(title, variables);
190 let formatted_message = format_template(body_template, variables);
191 json!({
192 "title": formatted_title,
193 "body": formatted_message
194 })
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use serde_json::json;
202
203 #[test]
204 fn test_slack_payload_builder() {
205 let title = "Test ${title_value}";
206 let message = "Test ${message_value}";
207 let variables = HashMap::from([
208 ("title_value".to_string(), "Title".to_string()),
209 ("message_value".to_string(), "Message".to_string()),
210 ]);
211 let payload = SlackPayloadBuilder.build_payload(title, message, &variables);
212 assert_eq!(
213 payload,
214 json!({
215 "blocks": [
216 {
217 "type": "section",
218 "text": {
219 "type": "mrkdwn",
220 "text": "*Test Title*\n\nTest Message"
221 }
222 }
223 ]
224 })
225 );
226 }
227
228 #[test]
229 fn test_discord_payload_builder() {
230 let title = "Test ${title_value}";
231 let message = "Test ${message_value}";
232 let variables = HashMap::from([
233 ("title_value".to_string(), "Title".to_string()),
234 ("message_value".to_string(), "Message".to_string()),
235 ]);
236 let payload = DiscordPayloadBuilder.build_payload(title, message, &variables);
237 assert_eq!(
238 payload,
239 json!({
240 "content": "*Test Title*\n\nTest Message"
241 })
242 );
243 }
244
245 #[test]
246 fn test_telegram_payload_builder() {
247 let builder = TelegramPayloadBuilder {
248 chat_id: "12345".to_string(),
249 disable_web_preview: true,
250 };
251 let title = "Test ${title_value}";
252 let message = "Test ${message_value}";
253 let variables = HashMap::from([
254 ("title_value".to_string(), "Title".to_string()),
255 ("message_value".to_string(), "Message".to_string()),
256 ]);
257 let payload = builder.build_payload(title, message, &variables);
258 assert_eq!(
259 payload,
260 json!({
261 "chat_id": "12345",
262 "text": "*Test Title* \n\nTest Message",
263 "parse_mode": "MarkdownV2",
264 "disable_web_page_preview": true
265 })
266 );
267 }
268
269 #[test]
270 fn test_generic_webhook_payload_builder() {
271 let title = "Test ${title_value}";
272 let message = "Test ${message_value}";
273 let variables = HashMap::from([
274 ("title_value".to_string(), "Title".to_string()),
275 ("message_value".to_string(), "Message".to_string()),
276 ]);
277 let payload = GenericWebhookPayloadBuilder.build_payload(title, message, &variables);
278 assert_eq!(
279 payload,
280 json!({
281 "title": "Test Title",
282 "body": "Test Message"
283 })
284 );
285 }
286
287 #[test]
288 fn test_escape_markdown_v2() {
289 assert_eq!(
291 TelegramPayloadBuilder::escape_markdown_v2(
292 "*Transaction Alert*\n*Network:* Base Sepolia\n*From:* 0x00001\n*To:* 0x00002\n*Transaction:* [View on Blockscout](https://base-sepolia.blockscout.com/tx/0x00003)"
293 ),
294 "*Transaction Alert*\n*Network:* Base Sepolia\n*From:* 0x00001\n*To:* 0x00002\n*Transaction:* [View on Blockscout](https://base\\-sepolia\\.blockscout\\.com/tx/0x00003)"
295 );
296
297 assert_eq!(
299 TelegramPayloadBuilder::escape_markdown_v2("Hello *world*!"),
300 "Hello *world*\\!"
301 );
302
303 assert_eq!(
305 TelegramPayloadBuilder::escape_markdown_v2("(test) [test] {test} <test>"),
306 "\\(test\\) \\[test\\] \\{test\\} <test\\>"
307 );
308
309 assert_eq!(
311 TelegramPayloadBuilder::escape_markdown_v2("```code block```"),
312 "```code block```"
313 );
314
315 assert_eq!(
317 TelegramPayloadBuilder::escape_markdown_v2("`inline code`"),
318 "`inline code`"
319 );
320
321 assert_eq!(
323 TelegramPayloadBuilder::escape_markdown_v2("*bold text*"),
324 "*bold text*"
325 );
326
327 assert_eq!(
329 TelegramPayloadBuilder::escape_markdown_v2("_italic text_"),
330 "_italic text_"
331 );
332
333 assert_eq!(
335 TelegramPayloadBuilder::escape_markdown_v2("~strikethrough~"),
336 "~strikethrough~"
337 );
338
339 assert_eq!(
341 TelegramPayloadBuilder::escape_markdown_v2("[link](https://example.com/test.html)"),
342 "[link](https://example\\.com/test\\.html)"
343 );
344
345 assert_eq!(
347 TelegramPayloadBuilder::escape_markdown_v2(
348 "[test!*_]{link}](https://test.com/path[1])"
349 ),
350 "\\[test\\!\\*\\_\\]\\{link\\}\\]\\(https://test\\.com/path\\[1\\]\\)"
351 );
352
353 assert_eq!(
355 TelegramPayloadBuilder::escape_markdown_v2(
356 "Hello *bold* and [link](http://test.com) and `code`"
357 ),
358 "Hello *bold* and [link](http://test\\.com) and `code`"
359 );
360
361 assert_eq!(
363 TelegramPayloadBuilder::escape_markdown_v2("test\\test"),
364 "test\\\\test"
365 );
366
367 assert_eq!(
369 TelegramPayloadBuilder::escape_markdown_v2("_*[]()~`>#+-=|{}.!\\"),
370 "\\_\\*\\[\\]\\(\\)\\~\\`\\>\\#\\+\\-\\=\\|\\{\\}\\.\\!\\\\",
371 );
372
373 assert_eq!(
375 TelegramPayloadBuilder::escape_markdown_v2("*bold with [link](http://test.com)*"),
376 "*bold with [link](http://test.com)*"
377 );
378
379 assert_eq!(TelegramPayloadBuilder::escape_markdown_v2(""), "");
381
382 assert_eq!(
384 TelegramPayloadBuilder::escape_markdown_v2("***"),
385 "**\\*" );
387 }
388
389 #[test]
390 fn test_events_match_reasons_single_event() {
391 let variables = HashMap::from([
392 (
393 "events.0.signature".to_string(),
394 "Transfer(address,address,uint256)".to_string(),
395 ),
396 ("events.0.args.from".to_string(), "0x1234".to_string()),
397 ("events.0.args.to".to_string(), "0x5678".to_string()),
398 ]);
399
400 let result = template_formatter::build_match_reasons(&variables, "events");
401 assert!(result.is_some());
402 let expected = "\n\n*Matched Events:*\n\n*Reason 1*\n\n*Signature:* `Transfer(address,address,uint256)`\n\n*Params:*\n\nfrom: `0x1234`\nto: `0x5678`";
403 assert_eq!(result.unwrap(), expected);
404 }
405
406 #[test]
407 fn test_events_match_reasons_multiple_events() {
408 let variables = HashMap::from([
409 (
410 "events.0.signature".to_string(),
411 "Transfer(address,address,uint256)".to_string(),
412 ),
413 ("events.0.args.from".to_string(), "0x1234".to_string()),
414 ("events.0.args.to".to_string(), "0x5678".to_string()),
415 (
416 "events.1.signature".to_string(),
417 "Approval(address,address,uint256)".to_string(),
418 ),
419 (
420 "events.1.args.owner".to_string(),
421 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
422 ),
423 (
424 "events.1.args.spender".to_string(),
425 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
426 ),
427 ("events.1.args.value".to_string(), "1000000000".to_string()),
428 (
429 "events.2.signature".to_string(),
430 "ValueChanged(uint256)".to_string(),
431 ),
432 ("events.2.args.value".to_string(), "1000000000".to_string()),
433 ]);
434
435 let result = template_formatter::build_match_reasons(&variables, "events");
436 assert!(result.is_some());
437 let expected = "\n\n*Matched Events:*\n\n*Reason 1*\n\n*Signature:* `Transfer(address,address,uint256)`\n\n*Params:*\n\nfrom: `0x1234`\nto: `0x5678`\n\n*Reason 2*\n\n*Signature:* `Approval(address,address,uint256)`\n\n*Params:*\n\nowner: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nvalue: `1000000000`\n\n*Reason 3*\n\n*Signature:* `ValueChanged(uint256)`\n\n*Params:*\n\nvalue: `1000000000`";
438 assert_eq!(result.unwrap(), expected);
439 }
440
441 #[test]
442 fn test_events_match_reasons_no_events() {
443 let variables = HashMap::from([
444 ("transaction.hash".to_string(), "0x1234".to_string()),
445 ("monitor.name".to_string(), "Test Monitor".to_string()),
446 ]);
447
448 let result = template_formatter::build_match_reasons(&variables, "events");
449 assert!(result.is_none());
450 }
451
452 #[test]
453 fn test_events_match_reasons_out_of_order() {
454 let variables = HashMap::from([
455 (
456 "events.2.signature".to_string(),
457 "ValueChanged(uint256)".to_string(),
458 ),
459 ("events.2.args.value".to_string(), "1000000000".to_string()),
460 (
461 "events.0.signature".to_string(),
462 "Transfer(address,address,uint256)".to_string(),
463 ),
464 ("events.0.args.from".to_string(), "0x1234".to_string()),
465 ("events.0.args.to".to_string(), "0x5678".to_string()),
466 (
467 "events.1.signature".to_string(),
468 "Approval(address,address,uint256)".to_string(),
469 ),
470 (
471 "events.1.args.owner".to_string(),
472 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
473 ),
474 (
475 "events.1.args.spender".to_string(),
476 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
477 ),
478 ("events.1.args.value".to_string(), "1000000000".to_string()),
479 ]);
480
481 let result = template_formatter::build_match_reasons(&variables, "events");
482 assert!(result.is_some());
483 let expected = "\n\n*Matched Events:*\n\n*Reason 1*\n\n*Signature:* `Transfer(address,address,uint256)`\n\n*Params:*\n\nfrom: `0x1234`\nto: `0x5678`\n\n*Reason 2*\n\n*Signature:* `Approval(address,address,uint256)`\n\n*Params:*\n\nowner: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nvalue: `1000000000`\n\n*Reason 3*\n\n*Signature:* `ValueChanged(uint256)`\n\n*Params:*\n\nvalue: `1000000000`";
484 assert_eq!(result.unwrap(), expected);
485 }
486
487 #[test]
488 fn test_format_template_with_all_events() {
489 let template = "Transaction detected: ${transaction.hash}\n\n${events}";
490 let variables = HashMap::from([
491 (
492 "transaction.hash".to_string(),
493 "0x1234567890abcdef".to_string(),
494 ),
495 (
496 "events.0.signature".to_string(),
497 "Transfer(address,address,uint256)".to_string(),
498 ),
499 ("events.0.args.from".to_string(), "0x1234".to_string()),
500 ("events.0.args.to".to_string(), "0x5678".to_string()),
501 (
502 "events.1.signature".to_string(),
503 "Approval(address,address,uint256)".to_string(),
504 ),
505 (
506 "events.1.args.owner".to_string(),
507 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
508 ),
509 (
510 "events.1.args.spender".to_string(),
511 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
512 ),
513 ("events.1.args.value".to_string(), "1000000000".to_string()),
514 ]);
515
516 let result = format_template(template, &variables);
517 let expected = "Transaction detected: 0x1234567890abcdef\n\n\n\n*Matched Events:*\n\n*Reason 1*\n\n*Signature:* `Transfer(address,address,uint256)`\n\n*Params:*\n\nfrom: `0x1234`\nto: `0x5678`\n\n*Reason 2*\n\n*Signature:* `Approval(address,address,uint256)`\n\n*Params:*\n\nowner: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nvalue: `1000000000`";
519 assert_eq!(result, expected);
520 }
521
522 #[test]
523 fn test_functions_match_reasons_single_function() {
524 let variables = HashMap::from([
525 (
526 "functions.0.signature".to_string(),
527 "transfer(address,uint256)".to_string(),
528 ),
529 ("functions.0.args.to".to_string(), "0x1234".to_string()),
530 ("functions.0.args.amount".to_string(), "1000000".to_string()),
531 ]);
532
533 let result = template_formatter::build_match_reasons(&variables, "functions");
534 assert!(result.is_some());
535 let expected = "\n\n*Matched Functions:*\n\n*Reason 1*\n\n*Signature:* `transfer(address,uint256)`\n\n*Params:*\n\namount: `1000000`\nto: `0x1234`";
536 assert_eq!(result.unwrap(), expected);
537 }
538
539 #[test]
540 fn test_functions_match_reasons_multiple_functions() {
541 let variables = HashMap::from([
542 (
543 "functions.0.signature".to_string(),
544 "transfer(address,uint256)".to_string(),
545 ),
546 ("functions.0.args.to".to_string(), "0x1234".to_string()),
547 ("functions.0.args.amount".to_string(), "1000000".to_string()),
548 (
549 "functions.1.signature".to_string(),
550 "approve(address,uint256)".to_string(),
551 ),
552 (
553 "functions.1.args.spender".to_string(),
554 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
555 ),
556 ("functions.1.args.amount".to_string(), "500000".to_string()),
557 (
558 "functions.2.signature".to_string(),
559 "mint(uint256)".to_string(),
560 ),
561 ("functions.2.args.amount".to_string(), "1000000".to_string()),
562 ]);
563
564 let result = template_formatter::build_match_reasons(&variables, "functions");
565 assert!(result.is_some());
566 let expected = "\n\n*Matched Functions:*\n\n*Reason 1*\n\n*Signature:* `transfer(address,uint256)`\n\n*Params:*\n\namount: `1000000`\nto: `0x1234`\n\n*Reason 2*\n\n*Signature:* `approve(address,uint256)`\n\n*Params:*\n\namount: `500000`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\n\n*Reason 3*\n\n*Signature:* `mint(uint256)`\n\n*Params:*\n\namount: `1000000`";
567 assert_eq!(result.unwrap(), expected);
568 }
569
570 #[test]
571 fn test_functions_match_reasons_no_functions() {
572 let variables = HashMap::from([
573 ("transaction.hash".to_string(), "0x1234".to_string()),
574 ("monitor.name".to_string(), "Test Monitor".to_string()),
575 ]);
576
577 let result = template_formatter::build_match_reasons(&variables, "functions");
578 assert!(result.is_none());
579 }
580
581 #[test]
582 fn test_functions_match_reasons_out_of_order() {
583 let variables = HashMap::from([
584 (
585 "functions.2.signature".to_string(),
586 "mint(uint256)".to_string(),
587 ),
588 ("functions.2.args.amount".to_string(), "1000000".to_string()),
589 (
590 "functions.0.signature".to_string(),
591 "transfer(address,uint256)".to_string(),
592 ),
593 ("functions.0.args.to".to_string(), "0x1234".to_string()),
594 ("functions.0.args.amount".to_string(), "1000000".to_string()),
595 (
596 "functions.1.signature".to_string(),
597 "approve(address,uint256)".to_string(),
598 ),
599 (
600 "functions.1.args.spender".to_string(),
601 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
602 ),
603 ("functions.1.args.amount".to_string(), "500000".to_string()),
604 ]);
605
606 let result = template_formatter::build_match_reasons(&variables, "functions");
607 assert!(result.is_some());
608 let expected = "\n\n*Matched Functions:*\n\n*Reason 1*\n\n*Signature:* `transfer(address,uint256)`\n\n*Params:*\n\namount: `1000000`\nto: `0x1234`\n\n*Reason 2*\n\n*Signature:* `approve(address,uint256)`\n\n*Params:*\n\namount: `500000`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\n\n*Reason 3*\n\n*Signature:* `mint(uint256)`\n\n*Params:*\n\namount: `1000000`";
609 assert_eq!(result.unwrap(), expected);
610 }
611
612 #[test]
613 fn test_format_template_with_all_functions() {
614 let template = "Transaction detected: ${transaction.hash}\n\n${functions}";
615 let variables = HashMap::from([
616 (
617 "transaction.hash".to_string(),
618 "0x1234567890abcdef".to_string(),
619 ),
620 (
621 "functions.0.signature".to_string(),
622 "transfer(address,uint256)".to_string(),
623 ),
624 ("functions.0.args.to".to_string(), "0x1234".to_string()),
625 ("functions.0.args.amount".to_string(), "1000000".to_string()),
626 (
627 "functions.1.signature".to_string(),
628 "approve(address,uint256)".to_string(),
629 ),
630 (
631 "functions.1.args.spender".to_string(),
632 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
633 ),
634 ("functions.1.args.amount".to_string(), "500000".to_string()),
635 ]);
636
637 let result = template_formatter::format_template(template, &variables);
638 let expected = "Transaction detected: 0x1234567890abcdef\n\n\n\n*Matched Functions:*\n\n*Reason 1*\n\n*Signature:* `transfer(address,uint256)`\n\n*Params:*\n\namount: `1000000`\nto: `0x1234`\n\n*Reason 2*\n\n*Signature:* `approve(address,uint256)`\n\n*Params:*\n\namount: `500000`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`";
640 assert_eq!(result, expected);
641 }
642
643 #[test]
644 fn test_format_template_with_functions_and_events() {
645 let template = "Transaction detected: ${transaction.hash}\n\n${functions}\n\n${events}";
646 let variables = HashMap::from([
647 (
648 "transaction.hash".to_string(),
649 "0x1234567890abcdef".to_string(),
650 ),
651 (
652 "events.0.signature".to_string(),
653 "Transfer(address,address,uint256)".to_string(),
654 ),
655 ("events.0.args.from".to_string(), "0x1234".to_string()),
656 ("events.0.args.to".to_string(), "0x5678".to_string()),
657 ("events.0.args.value".to_string(), "1000000".to_string()),
658 (
659 "events.1.signature".to_string(),
660 "Approval(address,address,uint256)".to_string(),
661 ),
662 (
663 "events.1.args.owner".to_string(),
664 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
665 ),
666 (
667 "events.1.args.spender".to_string(),
668 "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6".to_string(),
669 ),
670 ("events.1.args.value".to_string(), "500000".to_string()),
671 (
672 "functions.0.signature".to_string(),
673 "transfer(address,uint256)".to_string(),
674 ),
675 ("functions.0.args.to".to_string(), "0x9abc".to_string()),
676 ("functions.0.args.amount".to_string(), "750000".to_string()),
677 (
678 "functions.1.signature".to_string(),
679 "mint(uint256)".to_string(),
680 ),
681 ("functions.1.args.amount".to_string(), "250000".to_string()),
682 ]);
683
684 let result = template_formatter::format_template(template, &variables);
685 let expected = "Transaction detected: 0x1234567890abcdef\n\n\n\n*Matched Functions:*\n\n*Reason 1*\n\n*Signature:* `transfer(address,uint256)`\n\n*Params:*\n\namount: `750000`\nto: `0x9abc`\n\n*Reason 2*\n\n*Signature:* `mint(uint256)`\n\n*Params:*\n\namount: `250000`\n\n\n\n*Matched Events:*\n\n*Reason 1*\n\n*Signature:* `Transfer(address,address,uint256)`\n\n*Params:*\n\nfrom: `0x1234`\nto: `0x5678`\nvalue: `1000000`\n\n*Reason 2*\n\n*Signature:* `Approval(address,address,uint256)`\n\n*Params:*\n\nowner: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nspender: `0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6`\nvalue: `500000`";
688 assert_eq!(result, expected);
689 }
690
691 #[test]
692 fn test_format_template_with_no_events_removes_variable() {
693 let template = "Transaction detected: ${transaction.hash}\n\n${events}";
694 let variables = HashMap::from([
695 (
696 "transaction.hash".to_string(),
697 "0x1234567890abcdef".to_string(),
698 ),
699 ]);
701
702 let result = template_formatter::format_template(template, &variables);
703 let expected = "Transaction detected: 0x1234567890abcdef\n\n";
705 assert_eq!(result, expected);
706 }
707
708 #[test]
709 fn test_format_template_with_no_functions_removes_variable() {
710 let template = "Transaction detected: ${transaction.hash}\n\n${functions}";
711 let variables = HashMap::from([
712 (
713 "transaction.hash".to_string(),
714 "0x1234567890abcdef".to_string(),
715 ),
716 ]);
718
719 let result = template_formatter::format_template(template, &variables);
720 let expected = "Transaction detected: 0x1234567890abcdef\n\n";
722 assert_eq!(result, expected);
723 }
724
725 #[test]
726 fn test_build_match_reasons_no_index_part() {
727 let variables = HashMap::from([
728 ("events.0.args.from".to_string(), "0x1234".to_string()),
730 ("transaction.hash".to_string(), "0x1234".to_string()),
732 ("events.signature".to_string(), "Transfer".to_string()),
734 ]);
735
736 let result = template_formatter::build_match_reasons(&variables, "events");
737 assert!(result.is_none());
739 }
740
741 #[test]
742 fn test_build_match_reasons_no_signature() {
743 let variables = HashMap::from([
744 ("events.0.signature".to_string(), "".to_string()),
746 ("events.0.args.from".to_string(), "0x1234".to_string()),
748 ("events.0.args.to".to_string(), "0x5678".to_string()),
749 ]);
750
751 let result = template_formatter::build_match_reasons(&variables, "events");
752 let result_str = result.unwrap();
753
754 assert!(result_str.contains("\n\n*Matched Events:*\n"));
755 }
756
757 #[test]
758 fn test_build_match_reasons_mixed_valid_and_invalid() {
759 let variables = HashMap::from([
760 (
762 "events.0.signature".to_string(),
763 "Transfer(address,address,uint256)".to_string(),
764 ),
765 ("events.0.args.from".to_string(), "0x1234".to_string()),
766 ("events.1.signature".to_string(), "".to_string()),
768 ("events.1.args.to".to_string(), "0x5678".to_string()),
769 (
771 "events.2.signature".to_string(),
772 "Approval(address,address,uint256)".to_string(),
773 ),
774 ("events.2.args.owner".to_string(), "0x9abc".to_string()),
775 ]);
776
777 let result = template_formatter::build_match_reasons(&variables, "events");
778 assert!(result.is_some());
780 let result_str = result.unwrap();
781 assert!(result_str.contains("Transfer(address,address,uint256)"));
782 assert!(!result_str.contains("events.1")); assert!(result_str.contains("Approval(address,address,uint256)"));
784 }
785
786 #[test]
787 fn test_build_match_reasons_invalid_index_format() {
788 let variables = HashMap::from([
789 ("events.abc.signature".to_string(), "Transfer".to_string()),
791 ("events.-1.signature".to_string(), "Transfer".to_string()),
793 (
795 "events.0.signature".to_string(),
796 "Transfer(address,address,uint256)".to_string(),
797 ),
798 ("events.0.args.from".to_string(), "0x1234".to_string()),
799 ]);
800
801 let result = template_formatter::build_match_reasons(&variables, "events");
802 assert!(result.is_some());
804 let result_str = result.unwrap();
805 assert!(result_str.contains("Transfer(address,address,uint256)"));
806 assert!(!result_str.contains("abc")); assert!(!result_str.contains("-1")); }
809}