openzeppelin_monitor/models/blockchain/evm/
monitor.rs

1use crate::models::{
2	EVMReceiptLog, EVMTransaction, EVMTransactionReceipt, MatchConditions, Monitor,
3};
4use serde::{Deserialize, Serialize};
5
6/// Result of a successful monitor match on an EVM chain
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct EVMMonitorMatch {
9	/// Monitor configuration that triggered the match
10	pub monitor: Monitor,
11
12	/// Transaction that triggered the match
13	pub transaction: EVMTransaction,
14
15	/// Transaction receipt with execution results
16	pub receipt: Option<EVMTransactionReceipt>,
17
18	/// Transaction logs
19	pub logs: Option<Vec<EVMReceiptLog>>,
20
21	/// Network slug that the transaction was sent from
22	pub network_slug: String,
23
24	/// Conditions that were matched
25	pub matched_on: MatchConditions,
26
27	/// Decoded arguments from the matched conditions
28	pub matched_on_args: Option<MatchArguments>,
29}
30
31/// Collection of decoded parameters from matched conditions
32#[derive(Debug, Clone, Deserialize, Serialize)]
33pub struct MatchParamsMap {
34	/// Function or event signature
35	pub signature: String,
36
37	/// Decoded argument values
38	pub args: Option<Vec<MatchParamEntry>>,
39
40	/// Raw function/event signature as bytes
41	pub hex_signature: Option<String>,
42}
43
44/// Single decoded parameter from a function or event
45#[derive(Debug, Clone, Deserialize, Serialize)]
46pub struct MatchParamEntry {
47	/// Parameter name
48	pub name: String,
49
50	/// Parameter value
51	pub value: String,
52
53	/// Whether this is an indexed parameter (for events)
54	pub indexed: bool,
55
56	/// Parameter type (uint256, address, etc)
57	pub kind: String,
58}
59
60/// Arguments matched from functions and events
61#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct MatchArguments {
63	/// Matched function arguments
64	pub functions: Option<Vec<MatchParamsMap>>,
65
66	/// Matched event arguments
67	pub events: Option<Vec<MatchParamsMap>>,
68}
69
70/// Contract specification for an EVM smart contract
71///
72/// This structure represents the parsed specification of an EVM smart contract,
73/// following the Ethereum Contract ABI format. It contains information about all
74/// callable functions in the contract.
75#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
76pub struct ContractSpec(alloy::json_abi::JsonAbi);
77
78/// Convert a ContractSpec to an EVMContractSpec
79impl From<crate::models::ContractSpec> for ContractSpec {
80	fn from(spec: crate::models::ContractSpec) -> Self {
81		match spec {
82			crate::models::ContractSpec::EVM(evm_spec) => Self(evm_spec.0),
83			_ => Self(alloy::json_abi::JsonAbi::new()),
84		}
85	}
86}
87
88/// Convert a JsonAbi to a ContractSpec
89impl From<alloy::json_abi::JsonAbi> for ContractSpec {
90	fn from(spec: alloy::json_abi::JsonAbi) -> Self {
91		Self(spec)
92	}
93}
94
95/// Convert a serde_json::Value to a ContractSpec
96impl From<serde_json::Value> for ContractSpec {
97	fn from(spec: serde_json::Value) -> Self {
98		let spec = serde_json::from_value(spec).unwrap_or_else(|e| {
99			tracing::error!("Error parsing contract spec: {:?}", e);
100			alloy::json_abi::JsonAbi::new()
101		});
102		Self(spec)
103	}
104}
105
106/// Display a ContractSpec
107impl std::fmt::Display for ContractSpec {
108	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109		match serde_json::to_string(self) {
110			Ok(s) => write!(f, "{}", s),
111			Err(e) => {
112				tracing::error!("Error serializing contract spec: {:?}", e);
113				write!(f, "")
114			}
115		}
116	}
117}
118
119/// Dereference a ContractSpec
120impl std::ops::Deref for ContractSpec {
121	type Target = alloy::json_abi::JsonAbi;
122
123	fn deref(&self) -> &Self::Target {
124		&self.0
125	}
126}
127
128#[cfg(test)]
129mod tests {
130	use crate::{
131		models::{ContractSpec as ModelsContractSpec, FunctionCondition, StellarContractSpec},
132		utils::tests::evm::{
133			monitor::MonitorBuilder, receipt::ReceiptBuilder, transaction::TransactionBuilder,
134		},
135	};
136
137	use super::*;
138	use alloy::primitives::{Address, B256, U256, U64};
139
140	#[test]
141	fn test_evm_monitor_match() {
142		let monitor = MonitorBuilder::new()
143			.name("TestMonitor")
144			.function("transfer(address,uint256)", None)
145			.build();
146
147		let transaction = TransactionBuilder::new()
148			.hash(B256::with_last_byte(1))
149			.nonce(U256::from(1))
150			.from(Address::ZERO)
151			.to(Address::ZERO)
152			.value(U256::ZERO)
153			.gas_price(U256::from(20))
154			.gas_limit(U256::from(21000))
155			.build();
156
157		let receipt = ReceiptBuilder::new()
158			.transaction_hash(B256::with_last_byte(1))
159			.transaction_index(0)
160			.from(Address::ZERO)
161			.to(Address::ZERO)
162			.gas_used(U256::from(21000))
163			.status(true)
164			.build();
165
166		let match_params = MatchParamsMap {
167			signature: "transfer(address,uint256)".to_string(),
168			args: Some(vec![
169				MatchParamEntry {
170					name: "to".to_string(),
171					value: "0x0000000000000000000000000000000000000000".to_string(),
172					kind: "address".to_string(),
173					indexed: false,
174				},
175				MatchParamEntry {
176					name: "amount".to_string(),
177					value: "1000000000000000000".to_string(),
178					kind: "uint256".to_string(),
179					indexed: false,
180				},
181			]),
182			hex_signature: Some("0xa9059cbb".to_string()),
183		};
184
185		let monitor_match = EVMMonitorMatch {
186			monitor: monitor.clone(),
187			transaction: transaction.clone(),
188			receipt: Some(receipt.clone()),
189			logs: Some(receipt.logs.clone()),
190			network_slug: "ethereum_mainnet".to_string(),
191			matched_on: MatchConditions {
192				functions: vec![FunctionCondition {
193					signature: "transfer(address,uint256)".to_string(),
194					expression: None,
195				}],
196				events: vec![],
197				transactions: vec![],
198			},
199			matched_on_args: Some(MatchArguments {
200				functions: Some(vec![match_params]),
201				events: None,
202			}),
203		};
204
205		assert_eq!(monitor_match.monitor.name, "TestMonitor");
206		assert_eq!(monitor_match.transaction.hash, B256::with_last_byte(1));
207		assert_eq!(
208			monitor_match.receipt.as_ref().unwrap().status,
209			Some(U64::from(1))
210		);
211		assert_eq!(monitor_match.network_slug, "ethereum_mainnet");
212		assert_eq!(monitor_match.matched_on.functions.len(), 1);
213		assert_eq!(
214			monitor_match.matched_on.functions[0].signature,
215			"transfer(address,uint256)"
216		);
217
218		let matched_args = monitor_match.matched_on_args.unwrap();
219		let function_args = matched_args.functions.unwrap();
220		assert_eq!(function_args.len(), 1);
221		assert_eq!(function_args[0].signature, "transfer(address,uint256)");
222		assert_eq!(
223			function_args[0].hex_signature,
224			Some("0xa9059cbb".to_string())
225		);
226
227		let args = function_args[0].args.as_ref().unwrap();
228		assert_eq!(args.len(), 2);
229		assert_eq!(args[0].name, "to");
230		assert_eq!(args[0].kind, "address");
231		assert_eq!(args[1].name, "amount");
232		assert_eq!(args[1].kind, "uint256");
233	}
234
235	#[test]
236	fn test_match_arguments() {
237		let from_addr = Address::ZERO;
238		let to_addr = Address::with_last_byte(1);
239		let amount = U256::from(1000000000000000000u64);
240
241		let match_args = MatchArguments {
242			functions: Some(vec![MatchParamsMap {
243				signature: "transfer(address,uint256)".to_string(),
244				args: Some(vec![
245					MatchParamEntry {
246						name: "to".to_string(),
247						value: format!("{:#x}", to_addr),
248						kind: "address".to_string(),
249						indexed: false,
250					},
251					MatchParamEntry {
252						name: "amount".to_string(),
253						value: amount.to_string(),
254						kind: "uint256".to_string(),
255						indexed: false,
256					},
257				]),
258				hex_signature: Some("0xa9059cbb".to_string()),
259			}]),
260			events: Some(vec![MatchParamsMap {
261				signature: "Transfer(address,address,uint256)".to_string(),
262				args: Some(vec![
263					MatchParamEntry {
264						name: "from".to_string(),
265						value: format!("{:#x}", from_addr),
266						kind: "address".to_string(),
267						indexed: true,
268					},
269					MatchParamEntry {
270						name: "to".to_string(),
271						value: format!("{:#x}", to_addr),
272						kind: "address".to_string(),
273						indexed: true,
274					},
275					MatchParamEntry {
276						name: "amount".to_string(),
277						value: amount.to_string(),
278						kind: "uint256".to_string(),
279						indexed: false,
280					},
281				]),
282				hex_signature: Some(
283					"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
284						.to_string(),
285				),
286			}]),
287		};
288
289		assert!(match_args.functions.is_some());
290		let functions = match_args.functions.unwrap();
291		assert_eq!(functions.len(), 1);
292		assert_eq!(functions[0].signature, "transfer(address,uint256)");
293		assert_eq!(functions[0].hex_signature, Some("0xa9059cbb".to_string()));
294
295		let function_args = functions[0].args.as_ref().unwrap();
296		assert_eq!(function_args.len(), 2);
297		assert_eq!(function_args[0].name, "to");
298		assert_eq!(function_args[0].kind, "address");
299		assert_eq!(function_args[1].name, "amount");
300		assert_eq!(function_args[1].kind, "uint256");
301
302		assert!(match_args.events.is_some());
303		let events = match_args.events.unwrap();
304		assert_eq!(events.len(), 1);
305		assert_eq!(events[0].signature, "Transfer(address,address,uint256)");
306		assert_eq!(
307			events[0].hex_signature,
308			Some("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef".to_string())
309		);
310
311		let event_args = events[0].args.as_ref().unwrap();
312		assert_eq!(event_args.len(), 3);
313		assert_eq!(event_args[0].name, "from");
314		assert!(event_args[0].indexed);
315		assert_eq!(event_args[1].name, "to");
316		assert!(event_args[1].indexed);
317		assert_eq!(event_args[2].name, "amount");
318		assert!(!event_args[2].indexed);
319	}
320
321	#[test]
322	fn test_contract_spec_from_json() {
323		let json_value = serde_json::json!([{
324			"type": "function",
325			"name": "transfer",
326			"inputs": [
327				{
328					"name": "to",
329					"type": "address",
330					"internalType": "address"
331				},
332				{
333					"name": "amount",
334					"type": "uint256",
335					"internalType": "uint256"
336				}
337			],
338			"outputs": [],
339			"stateMutability": "nonpayable"
340		}]);
341
342		let contract_spec = ContractSpec::from(json_value);
343		let functions: Vec<_> = contract_spec.0.functions().collect();
344		assert!(!functions.is_empty());
345
346		let function = &functions[0];
347		assert_eq!(function.name, "transfer");
348		assert_eq!(function.inputs.len(), 2);
349		assert_eq!(function.inputs[0].name, "to");
350		assert_eq!(function.inputs[0].ty, "address");
351		assert_eq!(function.inputs[1].name, "amount");
352		assert_eq!(function.inputs[1].ty, "uint256");
353	}
354
355	#[test]
356	fn test_contract_spec_from_invalid_json() {
357		let invalid_json = serde_json::json!({
358			"invalid": "data"
359		});
360
361		let contract_spec = ContractSpec::from(invalid_json);
362		assert!(contract_spec.0.functions.is_empty());
363	}
364
365	#[test]
366	fn test_contract_spec_display() {
367		let json_value = serde_json::json!([{
368			"type": "function",
369			"name": "transfer",
370			"inputs": [
371				{
372					"name": "to",
373					"type": "address",
374					"internalType": "address"
375				}
376			],
377			"outputs": [],
378			"stateMutability": "nonpayable"
379		}]);
380
381		let contract_spec = ContractSpec::from(json_value);
382		let display_str = format!("{}", contract_spec);
383		assert!(!display_str.is_empty());
384		assert!(display_str.contains("transfer"));
385		assert!(display_str.contains("address"));
386	}
387
388	#[test]
389	fn test_contract_spec_deref() {
390		let json_value = serde_json::json!([{
391			"type": "function",
392			"name": "transfer",
393			"inputs": [
394				{
395					"name": "to",
396					"type": "address",
397					"internalType": "address"
398				}
399			],
400			"outputs": [],
401			"stateMutability": "nonpayable"
402		}]);
403
404		let contract_spec = ContractSpec::from(json_value);
405		let functions: Vec<_> = contract_spec.functions().collect();
406		assert!(!functions.is_empty());
407		assert_eq!(functions[0].name, "transfer");
408	}
409
410	#[test]
411	fn test_contract_spec_from_models() {
412		let json_value = serde_json::json!([{
413			"type": "function",
414			"name": "transfer",
415			"inputs": [
416				{
417					"name": "to",
418					"type": "address",
419					"internalType": "address"
420				}
421			],
422			"outputs": [],
423			"stateMutability": "nonpayable"
424		}]);
425
426		let evm_spec = ContractSpec::from(json_value.clone());
427		let models_spec = ModelsContractSpec::EVM(evm_spec);
428		let converted_spec = ContractSpec::from(models_spec);
429
430		let functions: Vec<_> = converted_spec.functions().collect();
431		assert!(!functions.is_empty());
432		assert_eq!(functions[0].name, "transfer");
433		assert_eq!(functions[0].inputs.len(), 1);
434		assert_eq!(functions[0].inputs[0].name, "to");
435		assert_eq!(functions[0].inputs[0].ty, "address");
436
437		let stellar_spec = StellarContractSpec::from(vec![]);
438		let models_spec = ModelsContractSpec::Stellar(stellar_spec);
439		let converted_spec = ContractSpec::from(models_spec);
440		assert!(converted_spec.is_empty());
441	}
442}