openzeppelin_monitor/services/filter/
filter_match.rs

1//! Match handling and processing logic.
2//!
3//! This module implements the processing of matched transactions and events:
4//! - Converts blockchain data to trigger-friendly format
5//! - Prepares notification payloads by converting blockchain-specific data into a generic format
6//! - Handles match execution through configured triggers
7//! - Manages the transformation of complex blockchain data into template variables
8
9use std::collections::HashMap;
10
11use alloy::primitives::Address;
12use serde_json::{json, Value as JsonValue};
13
14use crate::{
15	models::{MonitorMatch, ScriptLanguage},
16	services::{
17		filter::{
18			evm_helpers::{b256_to_string, h160_to_string},
19			FilterError,
20		},
21		trigger::TriggerExecutionServiceTrait,
22	},
23};
24
25/// Process a monitor match by executing associated triggers.
26///
27/// Takes a matched monitor event and processes it through the appropriate trigger service.
28/// Converts blockchain-specific data into a standardized format that can be used in trigger
29/// templates.
30///
31/// # Arguments
32/// * `matching_monitor` - The matched monitor event containing transaction and trigger information
33/// * `trigger_service` - Service responsible for executing triggers
34/// * `trigger_scripts` - Scripts to be executed for each trigger
35///
36/// # Returns
37/// Result indicating success or failure of trigger execution
38///
39/// # Example
40/// The function converts blockchain data into template variables like:
41/// ```text
42/// "monitor.name": "Transfer USDT Token"
43/// "transaction.hash": "0x99139c8f64b9b939678e261e1553660b502d9fd01c2ab1516e699ee6c8cc5791"
44/// "transaction.from": "0xf401346fd255e034a2e43151efe1d68c1e0f8ca5"
45/// "transaction.to": "0x0000000000001ff3684f28c67538d4d072c22734"
46/// "transaction.value": "24504000000000000"
47/// "events.0.signature": "Transfer(address,address,uint256)"
48/// "events.0.args.to": "0x70bf6634ee8cb27d04478f184b9b8bb13e5f4710"
49/// "events.0.args.from": "0x2e8135be71230c6b1b4045696d41c09db0414226"
50/// "events.0.args.value": "88248701"
51/// ```
52pub async fn handle_match<T: TriggerExecutionServiceTrait>(
53	matching_monitor: MonitorMatch,
54	trigger_service: &T,
55	trigger_scripts: &HashMap<String, (ScriptLanguage, String)>,
56) -> Result<(), FilterError> {
57	match &matching_monitor {
58		MonitorMatch::EVM(evm_monitor_match) => {
59			let transaction = evm_monitor_match.transaction.clone();
60			// If sender does not exist, we replace with 0x0000000000000000000000000000000000000000
61			let sender = transaction.sender().unwrap_or(&Address::ZERO);
62
63			// Create structured JSON data
64			let mut data_json = json!({
65				"monitor": {
66					"name": evm_monitor_match.monitor.name.clone(),
67				},
68				"transaction": {
69					"hash": b256_to_string(*transaction.hash()),
70					"from": h160_to_string(*sender),
71					"value": transaction.value().to_string(),
72				},
73				"functions": [],
74				"events": []
75			});
76
77			// Add 'to' address if present
78			if let Some(to) = transaction.to() {
79				data_json["transaction"]["to"] = json!(h160_to_string(*to));
80			}
81
82			// Process matched functions
83			let functions = data_json["functions"].as_array_mut().unwrap();
84			for func in evm_monitor_match.matched_on.functions.iter() {
85				let mut function_data = json!({
86					"signature": func.signature.clone(),
87					"args": {}
88				});
89
90				// Add function arguments if present
91				if let Some(args) = &evm_monitor_match.matched_on_args {
92					if let Some(func_args) = &args.functions {
93						for func_arg in func_args {
94							if func_arg.signature == func.signature {
95								if let Some(arg_entries) = &func_arg.args {
96									let args_obj = function_data["args"].as_object_mut().unwrap();
97									for arg in arg_entries {
98										args_obj.insert(arg.name.clone(), json!(arg.value.clone()));
99									}
100								}
101							}
102						}
103					}
104				}
105
106				functions.push(function_data);
107			}
108
109			// Process matched events
110			let events = data_json["events"].as_array_mut().unwrap();
111			for event in evm_monitor_match.matched_on.events.iter() {
112				let mut event_data = json!({
113					"signature": event.signature.clone(),
114					"args": {}
115				});
116
117				// Add event arguments if present
118				if let Some(args) = &evm_monitor_match.matched_on_args {
119					if let Some(event_args) = &args.events {
120						for event_arg in event_args {
121							if event_arg.signature == event.signature {
122								if let Some(arg_entries) = &event_arg.args {
123									let args_obj = event_data["args"].as_object_mut().unwrap();
124									for arg in arg_entries {
125										args_obj.insert(arg.name.clone(), json!(arg.value.clone()));
126									}
127								}
128							}
129						}
130					}
131				}
132
133				events.push(event_data);
134			}
135
136			// Swallow any errors since it's logged in the trigger service and we want to continue
137			// processing other matches
138			let _ = trigger_service
139				.execute(
140					&evm_monitor_match
141						.monitor
142						.triggers
143						.iter()
144						.map(|s| s.to_string())
145						.collect::<Vec<_>>(),
146					json_to_hashmap(&data_json),
147					&matching_monitor,
148					trigger_scripts,
149				)
150				.await;
151		}
152		MonitorMatch::Stellar(stellar_monitor_match) => {
153			let transaction = stellar_monitor_match.transaction.clone();
154
155			// Create structured JSON data
156			let mut data_json = json!({
157				"monitor": {
158					"name": stellar_monitor_match.monitor.name.clone(),
159				},
160				"transaction": {
161					"hash": transaction.hash().to_string(),
162				},
163				"functions": [],
164				"events": []
165			});
166
167			// Process matched functions
168			let functions = data_json["functions"].as_array_mut().unwrap();
169			for func in stellar_monitor_match.matched_on.functions.iter() {
170				let mut function_data = json!({
171					"signature": func.signature.clone(),
172					"args": {}
173				});
174
175				// Add function arguments if present
176				if let Some(args) = &stellar_monitor_match.matched_on_args {
177					if let Some(func_args) = &args.functions {
178						for func_arg in func_args {
179							if func_arg.signature == func.signature {
180								if let Some(arg_entries) = &func_arg.args {
181									let args_obj = function_data["args"].as_object_mut().unwrap();
182									for arg in arg_entries {
183										args_obj.insert(arg.name.clone(), json!(arg.value.clone()));
184									}
185								}
186							}
187						}
188					}
189				}
190
191				functions.push(function_data);
192			}
193
194			// Process matched events
195			let events = data_json["events"].as_array_mut().unwrap();
196			for event in stellar_monitor_match.matched_on.events.iter() {
197				let mut event_data = json!({
198					"signature": event.signature.clone(),
199					"args": {}
200				});
201
202				// Add event arguments if present
203				if let Some(args) = &stellar_monitor_match.matched_on_args {
204					if let Some(event_args) = &args.events {
205						for event_arg in event_args {
206							if event_arg.signature == event.signature {
207								if let Some(arg_entries) = &event_arg.args {
208									let args_obj = event_data["args"].as_object_mut().unwrap();
209									for arg in arg_entries {
210										args_obj.insert(arg.name.clone(), json!(arg.value.clone()));
211									}
212								}
213							}
214						}
215					}
216				}
217
218				events.push(event_data);
219			}
220
221			// Swallow any errors since it's logged in the trigger service and we want to continue
222			// processing other matches
223			let _ = trigger_service
224				.execute(
225					&stellar_monitor_match
226						.monitor
227						.triggers
228						.iter()
229						.map(|s| s.to_string())
230						.collect::<Vec<_>>(),
231					json_to_hashmap(&data_json),
232					&matching_monitor,
233					trigger_scripts,
234				)
235				.await;
236		}
237	}
238	Ok(())
239}
240
241/// Converts a JsonValue to a flattened HashMap with dotted path notation
242fn json_to_hashmap(json: &JsonValue) -> HashMap<String, String> {
243	let mut result = HashMap::new();
244	flatten_json_path(json, "", &mut result);
245	result
246}
247
248/// Flattens a JsonValue into a HashMap with dotted path notation
249fn flatten_json_path(value: &JsonValue, prefix: &str, result: &mut HashMap<String, String>) {
250	match value {
251		JsonValue::Object(obj) => {
252			for (key, val) in obj {
253				let new_prefix = if prefix.is_empty() {
254					key.clone()
255				} else {
256					format!("{}.{}", prefix, key)
257				};
258				flatten_json_path(val, &new_prefix, result);
259			}
260		}
261		JsonValue::Array(arr) => {
262			for (idx, val) in arr.iter().enumerate() {
263				let new_prefix = format!("{}.{}", prefix, idx);
264				flatten_json_path(val, &new_prefix, result);
265			}
266		}
267		JsonValue::String(s) => insert_primitive(prefix, result, s),
268		JsonValue::Number(n) => insert_primitive(prefix, result, n.to_string()),
269		JsonValue::Bool(b) => insert_primitive(prefix, result, b.to_string()),
270		JsonValue::Null => insert_primitive(prefix, result, "null".to_string()),
271	}
272}
273
274/// Helper function to insert primitive values with consistent key handling
275fn insert_primitive<T: ToString>(prefix: &str, result: &mut HashMap<String, String>, value: T) {
276	let key = if prefix.is_empty() {
277		"value".to_string()
278	} else {
279		prefix.to_string()
280	};
281	result.insert(key, value.to_string());
282}
283
284#[cfg(test)]
285mod tests {
286	use super::*;
287	use serde_json::json;
288
289	#[test]
290	fn test_json_to_hashmap() {
291		let json = json!({
292			"monitor": {
293				"name": "Test Monitor",
294			},
295			"transaction": {
296				"hash": "0x1234567890abcdef",
297			},
298		});
299
300		let hashmap = json_to_hashmap(&json);
301		assert_eq!(hashmap["monitor.name"], "Test Monitor");
302		assert_eq!(hashmap["transaction.hash"], "0x1234567890abcdef");
303	}
304
305	#[test]
306	fn test_json_to_hashmap_with_functions() {
307		let json = json!({
308			"monitor": {
309				"name": "Test Monitor",
310			},
311			"functions": [
312				{
313					"signature": "function1(uint256)",
314					"args": {
315						"arg1": "100",
316					},
317				},
318			],
319		});
320
321		let hashmap = json_to_hashmap(&json);
322		assert_eq!(hashmap["monitor.name"], "Test Monitor");
323		assert_eq!(hashmap["functions.0.signature"], "function1(uint256)");
324		assert_eq!(hashmap["functions.0.args.arg1"], "100");
325	}
326
327	#[test]
328	fn test_json_to_hashmap_with_events() {
329		let json = json!({
330			"monitor": {
331				"name": "Test Monitor",
332			},
333			"events": [
334				{
335					"signature": "event1(uint256)",
336					"args": {
337						"arg1": "100",
338					},
339				},
340			],
341		});
342
343		let hashmap = json_to_hashmap(&json);
344		assert_eq!(hashmap["monitor.name"], "Test Monitor");
345		assert_eq!(hashmap["events.0.signature"], "event1(uint256)");
346		assert_eq!(hashmap["events.0.args.arg1"], "100");
347	}
348
349	// Add tests for flatten_json_path
350	#[test]
351	fn test_flatten_json_path_object() {
352		let json = json!({
353			"monitor": {
354				"name": "Test Monitor",
355			},
356		});
357
358		let mut result = HashMap::new();
359		flatten_json_path(&json, "", &mut result);
360		assert_eq!(result["monitor.name"], "Test Monitor");
361	}
362
363	#[test]
364	fn test_flatten_json_path_array() {
365		let json = json!({
366			"monitor": {
367				"name": "Test Monitor",
368			},
369		});
370
371		let mut result = HashMap::new();
372		flatten_json_path(&json, "", &mut result);
373		assert_eq!(result["monitor.name"], "Test Monitor");
374	}
375
376	#[test]
377	fn test_flatten_json_path_string() {
378		let json = json!("Test String");
379		let mut result = HashMap::new();
380		flatten_json_path(&json, "test_prefix", &mut result);
381		assert_eq!(result["test_prefix"], "Test String");
382
383		let mut result2 = HashMap::new();
384		flatten_json_path(&json, "", &mut result2);
385		assert_eq!(result2["value"], "Test String");
386	}
387
388	#[test]
389	fn test_flatten_json_path_number() {
390		let json = json!(123);
391		let mut result = HashMap::new();
392		flatten_json_path(&json, "test_prefix", &mut result);
393		assert_eq!(result["test_prefix"], "123");
394
395		let mut result2 = HashMap::new();
396		flatten_json_path(&json, "", &mut result2);
397		assert_eq!(result2["value"], "123");
398	}
399
400	#[test]
401	fn test_flatten_json_path_boolean() {
402		let json = json!(true);
403		let mut result = HashMap::new();
404		flatten_json_path(&json, "test_prefix", &mut result);
405		assert_eq!(result["test_prefix"], "true");
406
407		// Test with empty prefix
408		let mut result2 = HashMap::new();
409		flatten_json_path(&json, "", &mut result2);
410		assert_eq!(result2["value"], "true");
411	}
412
413	#[test]
414	fn test_flatten_json_path_null() {
415		let json = json!(null);
416		let mut result = HashMap::new();
417		flatten_json_path(&json, "test_prefix", &mut result);
418		assert_eq!(result["test_prefix"], "null");
419
420		let mut result2 = HashMap::new();
421		flatten_json_path(&json, "", &mut result2);
422		assert_eq!(result2["value"], "null");
423	}
424
425	#[test]
426	fn test_flatten_json_path_nested_object() {
427		let json = json!({
428			"monitor": {
429				"name": "Test Monitor",
430				"nested": {
431					"key": "value",
432				},
433			},
434		});
435
436		let mut result = HashMap::new();
437		flatten_json_path(&json, "", &mut result);
438		assert_eq!(result["monitor.nested.key"], "value");
439	}
440
441	#[test]
442	fn test_flatten_json_path_nested_array() {
443		let json = json!({
444			"monitor": {
445				"name": "Test Monitor",
446				"nested": [
447					{
448						"key": "value1",
449					},
450					{
451						"key": "value2",
452					},
453				],
454			},
455		});
456
457		let mut result = HashMap::new();
458		flatten_json_path(&json, "", &mut result);
459		assert_eq!(result["monitor.nested.0.key"], "value1");
460		assert_eq!(result["monitor.nested.1.key"], "value2");
461	}
462
463	#[test]
464	fn test_flatten_json_path_with_prefix() {
465		let json = json!({
466			"monitor": {
467				"name": "Test Monitor",
468			},
469		});
470
471		let mut result = HashMap::new();
472		flatten_json_path(&json, "prefix", &mut result);
473		assert_eq!(result["prefix.monitor.name"], "Test Monitor");
474	}
475
476	#[test]
477	fn test_json_to_hashmap_with_primitive_values() {
478		// String
479		let json_string = json!("Test String");
480		let hashmap_string = json_to_hashmap(&json_string);
481		assert_eq!(hashmap_string["value"], "Test String");
482
483		// Number
484		let json_number = json!(123);
485		let hashmap_number = json_to_hashmap(&json_number);
486		assert_eq!(hashmap_number["value"], "123");
487
488		// Boolean
489		let json_bool = json!(true);
490		let hashmap_bool = json_to_hashmap(&json_bool);
491		assert_eq!(hashmap_bool["value"], "true");
492
493		// Null
494		let json_null = json!(null);
495		let hashmap_null = json_to_hashmap(&json_null);
496		assert_eq!(hashmap_null["value"], "null");
497	}
498
499	#[test]
500	fn test_insert_primitive() {
501		let mut result = HashMap::new();
502		insert_primitive("prefix", &mut result, "Test String");
503		assert_eq!(result["prefix"], "Test String");
504
505		let mut result2 = HashMap::new();
506		insert_primitive("", &mut result2, "Test String");
507		assert_eq!(result2["value"], "Test String");
508
509		let mut result3 = HashMap::new();
510		insert_primitive("prefix", &mut result3, 123);
511		assert_eq!(result3["prefix"], "123");
512
513		let mut result4 = HashMap::new();
514		insert_primitive("", &mut result4, 123);
515		assert_eq!(result4["value"], "123");
516
517		let mut result5 = HashMap::new();
518		insert_primitive("prefix", &mut result5, true);
519		assert_eq!(result5["prefix"], "true");
520
521		let mut result6 = HashMap::new();
522		insert_primitive("", &mut result6, true);
523		assert_eq!(result6["value"], "true");
524
525		let mut result7 = HashMap::new();
526		insert_primitive("prefix", &mut result7, JsonValue::Null);
527		assert_eq!(result7["prefix"], "null");
528
529		let mut result8 = HashMap::new();
530		insert_primitive("", &mut result8, JsonValue::Null);
531		assert_eq!(result8["value"], "null");
532	}
533}