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 MonitorMatch {
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/// EVM-specific configuration
129///
130/// This configuration is used to for additional fields in the monitor configuration
131/// that are specific to EVM.
132#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
133pub struct MonitorConfig {}
134
135#[cfg(test)]
136mod tests {
137	use crate::{
138		models::{ContractSpec as ModelsContractSpec, FunctionCondition, StellarContractSpec},
139		utils::tests::evm::{
140			monitor::MonitorBuilder, receipt::ReceiptBuilder, transaction::TransactionBuilder,
141		},
142	};
143
144	use super::*;
145	use alloy::primitives::{Address, B256, U256, U64};
146
147	#[test]
148	fn test_evm_monitor_match() {
149		let monitor = MonitorBuilder::new()
150			.name("TestMonitor")
151			.function("transfer(address,uint256)", None)
152			.build();
153
154		let transaction = TransactionBuilder::new()
155			.hash(B256::with_last_byte(1))
156			.nonce(U256::from(1))
157			.from(Address::ZERO)
158			.to(Address::ZERO)
159			.value(U256::ZERO)
160			.gas_price(U256::from(20))
161			.gas_limit(U256::from(21000))
162			.build();
163
164		let receipt = ReceiptBuilder::new()
165			.transaction_hash(B256::with_last_byte(1))
166			.transaction_index(0)
167			.from(Address::ZERO)
168			.to(Address::ZERO)
169			.gas_used(U256::from(21000))
170			.status(true)
171			.build();
172
173		let match_params = MatchParamsMap {
174			signature: "transfer(address,uint256)".to_string(),
175			args: Some(vec![
176				MatchParamEntry {
177					name: "to".to_string(),
178					value: "0x0000000000000000000000000000000000000000".to_string(),
179					kind: "address".to_string(),
180					indexed: false,
181				},
182				MatchParamEntry {
183					name: "amount".to_string(),
184					value: "1000000000000000000".to_string(),
185					kind: "uint256".to_string(),
186					indexed: false,
187				},
188			]),
189			hex_signature: Some("0xa9059cbb".to_string()),
190		};
191
192		let monitor_match = MonitorMatch {
193			monitor: monitor.clone(),
194			transaction: transaction.clone(),
195			receipt: Some(receipt.clone()),
196			logs: Some(receipt.logs.clone()),
197			network_slug: "ethereum_mainnet".to_string(),
198			matched_on: MatchConditions {
199				functions: vec![FunctionCondition {
200					signature: "transfer(address,uint256)".to_string(),
201					expression: None,
202				}],
203				events: vec![],
204				transactions: vec![],
205			},
206			matched_on_args: Some(MatchArguments {
207				functions: Some(vec![match_params]),
208				events: None,
209			}),
210		};
211
212		assert_eq!(monitor_match.monitor.name, "TestMonitor");
213		assert_eq!(monitor_match.transaction.hash, B256::with_last_byte(1));
214		assert_eq!(
215			monitor_match.receipt.as_ref().unwrap().status,
216			Some(U64::from(1))
217		);
218		assert_eq!(monitor_match.network_slug, "ethereum_mainnet");
219		assert_eq!(monitor_match.matched_on.functions.len(), 1);
220		assert_eq!(
221			monitor_match.matched_on.functions[0].signature,
222			"transfer(address,uint256)"
223		);
224
225		let matched_args = monitor_match.matched_on_args.unwrap();
226		let function_args = matched_args.functions.unwrap();
227		assert_eq!(function_args.len(), 1);
228		assert_eq!(function_args[0].signature, "transfer(address,uint256)");
229		assert_eq!(
230			function_args[0].hex_signature,
231			Some("0xa9059cbb".to_string())
232		);
233
234		let args = function_args[0].args.as_ref().unwrap();
235		assert_eq!(args.len(), 2);
236		assert_eq!(args[0].name, "to");
237		assert_eq!(args[0].kind, "address");
238		assert_eq!(args[1].name, "amount");
239		assert_eq!(args[1].kind, "uint256");
240	}
241
242	#[test]
243	fn test_match_arguments() {
244		let from_addr = Address::ZERO;
245		let to_addr = Address::with_last_byte(1);
246		let amount = U256::from(1000000000000000000u64);
247
248		let match_args = MatchArguments {
249			functions: Some(vec![MatchParamsMap {
250				signature: "transfer(address,uint256)".to_string(),
251				args: Some(vec![
252					MatchParamEntry {
253						name: "to".to_string(),
254						value: format!("{:#x}", to_addr),
255						kind: "address".to_string(),
256						indexed: false,
257					},
258					MatchParamEntry {
259						name: "amount".to_string(),
260						value: amount.to_string(),
261						kind: "uint256".to_string(),
262						indexed: false,
263					},
264				]),
265				hex_signature: Some("0xa9059cbb".to_string()),
266			}]),
267			events: Some(vec![MatchParamsMap {
268				signature: "Transfer(address,address,uint256)".to_string(),
269				args: Some(vec![
270					MatchParamEntry {
271						name: "from".to_string(),
272						value: format!("{:#x}", from_addr),
273						kind: "address".to_string(),
274						indexed: true,
275					},
276					MatchParamEntry {
277						name: "to".to_string(),
278						value: format!("{:#x}", to_addr),
279						kind: "address".to_string(),
280						indexed: true,
281					},
282					MatchParamEntry {
283						name: "amount".to_string(),
284						value: amount.to_string(),
285						kind: "uint256".to_string(),
286						indexed: false,
287					},
288				]),
289				hex_signature: Some(
290					"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
291						.to_string(),
292				),
293			}]),
294		};
295
296		assert!(match_args.functions.is_some());
297		let functions = match_args.functions.unwrap();
298		assert_eq!(functions.len(), 1);
299		assert_eq!(functions[0].signature, "transfer(address,uint256)");
300		assert_eq!(functions[0].hex_signature, Some("0xa9059cbb".to_string()));
301
302		let function_args = functions[0].args.as_ref().unwrap();
303		assert_eq!(function_args.len(), 2);
304		assert_eq!(function_args[0].name, "to");
305		assert_eq!(function_args[0].kind, "address");
306		assert_eq!(function_args[1].name, "amount");
307		assert_eq!(function_args[1].kind, "uint256");
308
309		assert!(match_args.events.is_some());
310		let events = match_args.events.unwrap();
311		assert_eq!(events.len(), 1);
312		assert_eq!(events[0].signature, "Transfer(address,address,uint256)");
313		assert_eq!(
314			events[0].hex_signature,
315			Some("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef".to_string())
316		);
317
318		let event_args = events[0].args.as_ref().unwrap();
319		assert_eq!(event_args.len(), 3);
320		assert_eq!(event_args[0].name, "from");
321		assert!(event_args[0].indexed);
322		assert_eq!(event_args[1].name, "to");
323		assert!(event_args[1].indexed);
324		assert_eq!(event_args[2].name, "amount");
325		assert!(!event_args[2].indexed);
326	}
327
328	#[test]
329	fn test_contract_spec_from_json() {
330		let json_value = serde_json::json!([{
331			"type": "function",
332			"name": "transfer",
333			"inputs": [
334				{
335					"name": "to",
336					"type": "address",
337					"internalType": "address"
338				},
339				{
340					"name": "amount",
341					"type": "uint256",
342					"internalType": "uint256"
343				}
344			],
345			"outputs": [],
346			"stateMutability": "nonpayable"
347		}]);
348
349		let contract_spec = ContractSpec::from(json_value);
350		let functions: Vec<_> = contract_spec.0.functions().collect();
351		assert!(!functions.is_empty());
352
353		let function = &functions[0];
354		assert_eq!(function.name, "transfer");
355		assert_eq!(function.inputs.len(), 2);
356		assert_eq!(function.inputs[0].name, "to");
357		assert_eq!(function.inputs[0].ty, "address");
358		assert_eq!(function.inputs[1].name, "amount");
359		assert_eq!(function.inputs[1].ty, "uint256");
360	}
361
362	#[test]
363	fn test_contract_spec_from_invalid_json() {
364		let invalid_json = serde_json::json!({
365			"invalid": "data"
366		});
367
368		let contract_spec = ContractSpec::from(invalid_json);
369		assert!(contract_spec.0.functions.is_empty());
370	}
371
372	#[test]
373	fn test_contract_spec_display() {
374		let json_value = serde_json::json!([{
375			"type": "function",
376			"name": "transfer",
377			"inputs": [
378				{
379					"name": "to",
380					"type": "address",
381					"internalType": "address"
382				}
383			],
384			"outputs": [],
385			"stateMutability": "nonpayable"
386		}]);
387
388		let contract_spec = ContractSpec::from(json_value);
389		let display_str = format!("{}", contract_spec);
390		assert!(!display_str.is_empty());
391		assert!(display_str.contains("transfer"));
392		assert!(display_str.contains("address"));
393	}
394
395	#[test]
396	fn test_contract_spec_deref() {
397		let json_value = serde_json::json!([{
398			"type": "function",
399			"name": "transfer",
400			"inputs": [
401				{
402					"name": "to",
403					"type": "address",
404					"internalType": "address"
405				}
406			],
407			"outputs": [],
408			"stateMutability": "nonpayable"
409		}]);
410
411		let contract_spec = ContractSpec::from(json_value);
412		let functions: Vec<_> = contract_spec.functions().collect();
413		assert!(!functions.is_empty());
414		assert_eq!(functions[0].name, "transfer");
415	}
416
417	#[test]
418	fn test_contract_spec_from_models() {
419		let json_value = serde_json::json!([{
420			"type": "function",
421			"name": "transfer",
422			"inputs": [
423				{
424					"name": "to",
425					"type": "address",
426					"internalType": "address"
427				}
428			],
429			"outputs": [],
430			"stateMutability": "nonpayable"
431		}]);
432
433		let evm_spec = ContractSpec::from(json_value.clone());
434		let models_spec = ModelsContractSpec::EVM(evm_spec);
435		let converted_spec = ContractSpec::from(models_spec);
436
437		let functions: Vec<_> = converted_spec.functions().collect();
438		assert!(!functions.is_empty());
439		assert_eq!(functions[0].name, "transfer");
440		assert_eq!(functions[0].inputs.len(), 1);
441		assert_eq!(functions[0].inputs[0].name, "to");
442		assert_eq!(functions[0].inputs[0].ty, "address");
443
444		let stellar_spec = StellarContractSpec::from(vec![]);
445		let models_spec = ModelsContractSpec::Stellar(stellar_spec);
446		let converted_spec = ContractSpec::from(models_spec);
447		assert!(converted_spec.is_empty());
448	}
449}