openzeppelin_monitor/models/blockchain/stellar/
monitor.rs

1//! Monitor implementation for Stellar blockchain.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use stellar_xdr::curr::ScSpecEntry;
6
7use crate::{
8	models::{MatchConditions, Monitor, StellarBlock, StellarTransaction},
9	services::filter::stellar_helpers::{
10		get_contract_spec_events, get_contract_spec_functions,
11		get_contract_spec_with_event_parameters, get_contract_spec_with_function_input_parameters,
12	},
13};
14
15/// Result of a successful monitor match on a Stellar chain
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct MonitorMatch {
18	/// Monitor configuration that triggered the match
19	pub monitor: Monitor,
20
21	/// Transaction that triggered the match
22	pub transaction: StellarTransaction,
23
24	/// Ledger containing the matched transaction
25	pub ledger: StellarBlock,
26
27	/// Network slug that the transaction was sent from
28	pub network_slug: String,
29
30	/// Conditions that were matched
31	pub matched_on: MatchConditions,
32
33	/// Decoded arguments from the matched conditions
34	pub matched_on_args: Option<MatchArguments>,
35}
36
37/// Collection of decoded parameters from matched conditions
38#[derive(Debug, Clone, Deserialize, Serialize)]
39pub struct MatchParamsMap {
40	/// Function or event signature
41	pub signature: String,
42
43	/// Decoded argument values
44	pub args: Option<Vec<MatchParamEntry>>,
45}
46
47/// Single decoded parameter from a function or event
48#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct MatchParamEntry {
50	/// Parameter name
51	pub name: String,
52
53	/// Parameter value
54	pub value: String,
55
56	/// Parameter type
57	pub kind: String,
58
59	/// Whether this is an indexed parameter
60	pub indexed: bool,
61}
62
63/// Arguments matched from functions and events
64#[derive(Debug, Clone, Deserialize, Serialize)]
65pub struct MatchArguments {
66	/// Matched function arguments
67	pub functions: Option<Vec<MatchParamsMap>>,
68
69	/// Matched event arguments
70	pub events: Option<Vec<MatchParamsMap>>,
71}
72
73/// Parsed result of a Stellar contract operation
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ParsedOperationResult {
76	/// Address of the contract that was called
77	pub contract_address: String,
78
79	/// Name of the function that was called
80	pub function_name: String,
81
82	/// Full function signature
83	pub function_signature: String,
84
85	/// Decoded function arguments
86	pub arguments: Vec<Value>,
87}
88
89/// Decoded parameter from a Stellar contract function or event
90///
91/// This structure represents a single decoded parameter from a contract interaction,
92/// providing the parameter's value, type information, and indexing status.
93/// Similar to EVM event/function parameters but adapted for Stellar's type system.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct DecodedParamEntry {
96	/// String representation of the parameter value
97	pub value: String,
98
99	/// Parameter type (e.g., "address", "i128", "bytes")
100	pub kind: String,
101
102	/// Whether this parameter is indexed (for event topics)
103	pub indexed: bool,
104}
105
106/// Raw contract specification for a Stellar smart contract
107///
108/// This structure represents the native Stellar contract specification format, derived directly
109/// from ScSpecEntry. It contains the raw contract interface data as provided by the Stellar
110/// blockchain, including all function definitions, types, and other contract metadata in their
111/// original format.
112#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
113pub struct ContractSpec(Vec<ScSpecEntry>);
114
115impl From<Vec<ScSpecEntry>> for ContractSpec {
116	fn from(spec: Vec<ScSpecEntry>) -> Self {
117		ContractSpec(spec)
118	}
119}
120
121/// Convert a ContractSpec to a StellarContractSpec
122impl From<crate::models::ContractSpec> for ContractSpec {
123	fn from(spec: crate::models::ContractSpec) -> Self {
124		match spec {
125			crate::models::ContractSpec::Stellar(stellar_spec) => Self(stellar_spec.0),
126			_ => Self(Vec::new()),
127		}
128	}
129}
130
131/// Convert a serde_json::Value to a StellarContractSpec
132impl From<serde_json::Value> for ContractSpec {
133	fn from(spec: serde_json::Value) -> Self {
134		let spec = serde_json::from_value(spec).unwrap_or_else(|e| {
135			tracing::error!("Error parsing contract spec: {:?}", e);
136			Vec::new()
137		});
138		Self(spec)
139	}
140}
141
142/// Display a StellarContractSpec
143impl std::fmt::Display for ContractSpec {
144	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145		match serde_json::to_string(self) {
146			Ok(s) => write!(f, "{}", s),
147			Err(e) => {
148				tracing::error!("Error serializing contract spec: {:?}", e);
149				write!(f, "")
150			}
151		}
152	}
153}
154
155/// Dereference a StellarContractSpec
156impl std::ops::Deref for ContractSpec {
157	type Target = Vec<ScSpecEntry>;
158
159	fn deref(&self) -> &Self::Target {
160		&self.0
161	}
162}
163
164/// Human-readable contract specification for a Stellar smart contract
165///
166/// This structure provides a simplified, application-specific view of a Stellar contract's
167/// interface. It transforms the raw ContractSpec into a more accessible format that's easier
168/// to work with in our monitoring system. The main differences are:
169/// - Focuses on callable functions with their input parameters
170/// - Provides a cleaner, more structured representation
171/// - Optimized for our specific use case of monitoring contract interactions
172#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
173pub struct FormattedContractSpec {
174	/// List of callable functions defined in the contract
175	pub functions: Vec<ContractFunction>,
176
177	/// List of events defined in the contract
178	pub events: Vec<ContractEvent>,
179}
180
181impl From<ContractSpec> for FormattedContractSpec {
182	fn from(spec: ContractSpec) -> Self {
183		let functions = get_contract_spec_with_function_input_parameters(
184			get_contract_spec_functions(spec.0.clone()),
185		);
186
187		let events =
188			get_contract_spec_with_event_parameters(get_contract_spec_events(spec.0.clone()));
189
190		FormattedContractSpec { functions, events }
191	}
192}
193
194/// Function definition within a Stellar contract specification
195///
196/// Represents a callable function in a Stellar smart contract, including its name
197/// and input parameters. This is parsed from the contract's ScSpecFunctionV0 entries
198/// and provides a more accessible format for working with contract interfaces.
199///
200/// # Example
201/// ```ignore
202/// {
203///     "name": "transfer",
204///     "inputs": [
205///         {"index": 0, "name": "to", "kind": "Address"},
206///         {"index": 1, "name": "amount", "kind": "U64"}
207///     ],
208///     "signature": "transfer(Address,U64)"
209/// }
210/// ```
211#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
212pub struct ContractFunction {
213	/// Name of the function as defined in the contract
214	pub name: String,
215
216	/// Ordered list of input parameters accepted by the function
217	pub inputs: Vec<ContractInput>,
218
219	/// Signature of the function
220	pub signature: String,
221}
222
223/// Input parameter specification for a Stellar contract function
224///
225/// Describes a single parameter in a contract function, including its position,
226/// name, and type. The type (kind) follows Stellar's type system and can include
227/// basic types (U64, I64, Address, etc.) as well as complex types (Vec, Map, etc.).
228///
229/// # Type Examples
230/// - Basic types: "U64", "I64", "Address", "Bool", "String"
231/// - Complex types: "`Vec<Address>`", "`Map<String,U64>`", "`Bytes32`"
232#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
233pub struct ContractInput {
234	/// Zero-based index of the parameter in the function signature
235	pub index: u32,
236
237	/// Parameter name as defined in the contract
238	pub name: String,
239
240	/// Parameter type in Stellar's type system format
241	pub kind: String,
242}
243
244/// Event parameter location (indexed or data)
245#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
246pub enum EventParamLocation {
247	/// Parameter is indexed (in topics)
248	#[default]
249	Indexed,
250	/// Parameter is in event data
251	Data,
252}
253
254/// Event parameter specification for a Stellar contract event
255///
256/// Describes a single parameter in a contract event, including its name,
257/// type, and whether it's indexed or in the event data.
258#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
259pub struct ContractEventParam {
260	/// Parameter name as defined in the contract
261	pub name: String,
262
263	/// Parameter type in Stellar's type system format
264	pub kind: String,
265
266	/// Whether this parameter is indexed or in data
267	pub location: EventParamLocation,
268}
269
270/// Event definition within a Stellar contract specification
271///
272/// Represents an event that can be emitted by a Stellar smart contract,
273/// including its name and parameters. This is parsed from the contract's
274/// ScSpecEventV0 entries.
275#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
276pub struct ContractEvent {
277	/// Name of the event as defined in the contract
278	pub name: String,
279
280	/// Prefix topics that are emitted before indexed parameters
281	/// These are metadata topics used to identify the event
282	pub prefix_topics: Vec<String>,
283
284	/// Ordered list of parameters in the event
285	pub params: Vec<ContractEventParam>,
286
287	/// Signature of the event
288	pub signature: String,
289}
290
291/// Stellar-specific configuration
292///
293/// This configuration is used to for additional fields in the monitor configuration
294/// that are specific to Stellar.
295#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
296pub struct MonitorConfig {}
297
298#[cfg(test)]
299mod tests {
300	use super::*;
301	use crate::models::EVMContractSpec;
302	use crate::models::{
303		blockchain::stellar::block::LedgerInfo as StellarLedgerInfo,
304		blockchain::stellar::transaction::TransactionInfo as StellarTransactionInfo,
305		ContractSpec as ModelsContractSpec, FunctionCondition, MatchConditions,
306	};
307	use crate::utils::tests::builders::stellar::monitor::MonitorBuilder;
308	use serde_json::json;
309	use stellar_xdr::curr::{ScSpecEntry, ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeDef};
310
311	#[test]
312	fn test_contract_spec_from_vec() {
313		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
314			name: "test_function".try_into().unwrap(),
315			inputs: vec![].try_into().unwrap(),
316			outputs: vec![].try_into().unwrap(),
317			doc: "Test function documentation".try_into().unwrap(),
318		})];
319
320		let contract_spec = ContractSpec::from(spec_entries.clone());
321		assert_eq!(contract_spec.0, spec_entries);
322	}
323
324	#[test]
325	fn test_contract_spec_from_json() {
326		let json_value = serde_json::json!([
327			{
328				"function_v0": {
329					"doc": "Test function documentation",
330					"name": "test_function",
331					"inputs": [
332						{
333							"doc": "",
334							"name": "from",
335							"type_": "address"
336						},
337						{
338							"doc": "",
339							"name": "to",
340							"type_": "address"
341						},
342						{
343							"doc": "",
344							"name": "amount",
345							"type_": "i128"
346						}
347					],
348					"outputs": []
349				}
350			},
351		]);
352
353		let contract_spec = ContractSpec::from(json_value);
354		assert!(!contract_spec.0.is_empty());
355		if let ScSpecEntry::FunctionV0(func) = &contract_spec.0[0] {
356			assert_eq!(func.name.to_string(), "test_function");
357			assert_eq!(func.doc.to_string(), "Test function documentation");
358		} else {
359			panic!("Expected FunctionV0 entry");
360		}
361	}
362
363	#[test]
364	fn test_contract_spec_from_invalid_json() {
365		let invalid_json = serde_json::json!({
366			"invalid": "data"
367		});
368
369		let contract_spec = ContractSpec::from(invalid_json);
370		assert!(contract_spec.0.is_empty());
371	}
372
373	#[test]
374	fn test_formatted_contract_spec_from_contract_spec() {
375		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
376			name: "transfer".try_into().unwrap(),
377			inputs: vec![
378				ScSpecFunctionInputV0 {
379					name: "to".try_into().unwrap(),
380					type_: ScSpecTypeDef::Address,
381					doc: "Recipient address".try_into().unwrap(),
382				},
383				ScSpecFunctionInputV0 {
384					name: "amount".try_into().unwrap(),
385					type_: ScSpecTypeDef::U64,
386					doc: "Amount to transfer".try_into().unwrap(),
387				},
388			]
389			.try_into()
390			.unwrap(),
391			outputs: vec![].try_into().unwrap(),
392			doc: "Transfer function documentation".try_into().unwrap(),
393		})];
394
395		let contract_spec = ContractSpec(spec_entries);
396		let formatted_spec = FormattedContractSpec::from(contract_spec);
397
398		assert_eq!(formatted_spec.functions.len(), 1);
399		let function = &formatted_spec.functions[0];
400		assert_eq!(function.name, "transfer");
401		assert_eq!(function.inputs.len(), 2);
402		assert_eq!(function.inputs[0].name, "to");
403		assert_eq!(function.inputs[0].kind, "Address");
404		assert_eq!(function.inputs[1].name, "amount");
405		assert_eq!(function.inputs[1].kind, "U64");
406		assert_eq!(function.signature, "transfer(Address,U64)");
407	}
408
409	#[test]
410	fn test_contract_spec_display() {
411		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
412			name: "test_function".try_into().unwrap(),
413			inputs: vec![].try_into().unwrap(),
414			outputs: vec![].try_into().unwrap(),
415			doc: "Test function documentation".try_into().unwrap(),
416		})];
417
418		let contract_spec = ContractSpec(spec_entries);
419		let display_str = format!("{}", contract_spec);
420		assert!(!display_str.is_empty());
421		assert!(display_str.contains("test_function"));
422	}
423
424	#[test]
425	fn test_contract_spec_with_multiple_functions() {
426		let spec_entries = vec![
427			ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
428				name: "transfer".try_into().unwrap(),
429				inputs: vec![
430					ScSpecFunctionInputV0 {
431						name: "to".try_into().unwrap(),
432						type_: ScSpecTypeDef::Address,
433						doc: "Recipient address".try_into().unwrap(),
434					},
435					ScSpecFunctionInputV0 {
436						name: "amount".try_into().unwrap(),
437						type_: ScSpecTypeDef::U64,
438						doc: "Amount to transfer".try_into().unwrap(),
439					},
440				]
441				.try_into()
442				.unwrap(),
443				outputs: vec![].try_into().unwrap(),
444				doc: "Transfer function".try_into().unwrap(),
445			}),
446			ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
447				name: "balance".try_into().unwrap(),
448				inputs: vec![ScSpecFunctionInputV0 {
449					name: "account".try_into().unwrap(),
450					type_: ScSpecTypeDef::Address,
451					doc: "Account to check balance for".try_into().unwrap(),
452				}]
453				.try_into()
454				.unwrap(),
455				outputs: vec![ScSpecTypeDef::U64].try_into().unwrap(),
456				doc: "Balance function".try_into().unwrap(),
457			}),
458		];
459
460		let contract_spec = ContractSpec(spec_entries);
461		let formatted_spec = FormattedContractSpec::from(contract_spec);
462
463		assert_eq!(formatted_spec.functions.len(), 2);
464
465		let transfer_fn = formatted_spec
466			.functions
467			.iter()
468			.find(|f| f.name == "transfer")
469			.expect("Transfer function not found");
470		assert_eq!(transfer_fn.signature, "transfer(Address,U64)");
471
472		let balance_fn = formatted_spec
473			.functions
474			.iter()
475			.find(|f| f.name == "balance")
476			.expect("Balance function not found");
477		assert_eq!(balance_fn.signature, "balance(Address)");
478	}
479
480	#[test]
481	fn test_monitor_match() {
482		let monitor = MonitorBuilder::new()
483			.name("TestMonitor")
484			.function("transfer(address,uint256)", None)
485			.build();
486
487		let transaction = StellarTransaction(StellarTransactionInfo {
488			status: "SUCCESS".to_string(),
489			transaction_hash: "test_hash".to_string(),
490			application_order: 1,
491			fee_bump: false,
492			envelope_xdr: Some("mock_xdr".to_string()),
493			envelope_json: Some(serde_json::json!({
494				"type": "ENVELOPE_TYPE_TX",
495				"tx": {
496					"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
497					"operations": [{
498						"type": "invokeHostFunction",
499						"function": "transfer",
500						"parameters": ["GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "1000000"]
501					}]
502				}
503			})),
504			result_xdr: Some("mock_result".to_string()),
505			result_json: None,
506			result_meta_xdr: Some("mock_meta".to_string()),
507			result_meta_json: None,
508			diagnostic_events_xdr: None,
509			diagnostic_events_json: None,
510			ledger: 123,
511			ledger_close_time: 1234567890,
512			decoded: None,
513		});
514
515		let ledger = StellarBlock(StellarLedgerInfo {
516			hash: "test_ledger_hash".to_string(),
517			sequence: 123,
518			ledger_close_time: "2024-03-20T12:00:00Z".to_string(),
519			ledger_header: "mock_header".to_string(),
520			ledger_header_json: None,
521			ledger_metadata: "mock_metadata".to_string(),
522			ledger_metadata_json: None,
523		});
524
525		let match_params = MatchParamsMap {
526			signature: "transfer(address,uint256)".to_string(),
527			args: Some(vec![
528				MatchParamEntry {
529					name: "to".to_string(),
530					value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
531					kind: "Address".to_string(),
532					indexed: false,
533				},
534				MatchParamEntry {
535					name: "amount".to_string(),
536					value: "1000000".to_string(),
537					kind: "U64".to_string(),
538					indexed: false,
539				},
540			]),
541		};
542
543		let monitor_match = MonitorMatch {
544			monitor: monitor.clone(),
545			transaction: transaction.clone(),
546			ledger: ledger.clone(),
547			network_slug: "stellar_mainnet".to_string(),
548			matched_on: MatchConditions {
549				functions: vec![FunctionCondition {
550					signature: "transfer(address,uint256)".to_string(),
551					expression: None,
552				}],
553				events: vec![],
554				transactions: vec![],
555			},
556			matched_on_args: Some(MatchArguments {
557				functions: Some(vec![match_params]),
558				events: None,
559			}),
560		};
561
562		assert_eq!(monitor_match.monitor.name, "TestMonitor");
563		assert_eq!(monitor_match.transaction.transaction_hash, "test_hash");
564		assert_eq!(monitor_match.ledger.sequence, 123);
565		assert_eq!(monitor_match.network_slug, "stellar_mainnet");
566		assert_eq!(monitor_match.matched_on.functions.len(), 1);
567		assert_eq!(
568			monitor_match.matched_on.functions[0].signature,
569			"transfer(address,uint256)"
570		);
571
572		let matched_args = monitor_match.matched_on_args.unwrap();
573		let function_args = matched_args.functions.unwrap();
574		assert_eq!(function_args.len(), 1);
575		assert_eq!(function_args[0].signature, "transfer(address,uint256)");
576
577		let args = function_args[0].args.as_ref().unwrap();
578		assert_eq!(args.len(), 2);
579		assert_eq!(args[0].name, "to");
580		assert_eq!(args[0].kind, "Address");
581		assert_eq!(args[1].name, "amount");
582		assert_eq!(args[1].kind, "U64");
583	}
584
585	#[test]
586	fn test_parsed_operation_result() {
587		let result = ParsedOperationResult {
588			contract_address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
589				.to_string(),
590			function_name: "transfer".to_string(),
591			function_signature: "transfer(address,uint256)".to_string(),
592			arguments: vec![
593				serde_json::json!("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"),
594				serde_json::json!("1000000"),
595			],
596		};
597
598		assert_eq!(
599			result.contract_address,
600			"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
601		);
602		assert_eq!(result.function_name, "transfer");
603		assert_eq!(result.function_signature, "transfer(address,uint256)");
604		assert_eq!(result.arguments.len(), 2);
605		assert_eq!(
606			result.arguments[0],
607			"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
608		);
609		assert_eq!(result.arguments[1], "1000000");
610	}
611
612	#[test]
613	fn test_decoded_param_entry() {
614		let param = DecodedParamEntry {
615			value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
616			kind: "Address".to_string(),
617			indexed: false,
618		};
619
620		assert_eq!(
621			param.value,
622			"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
623		);
624		assert_eq!(param.kind, "Address");
625		assert!(!param.indexed);
626	}
627
628	#[test]
629	fn test_match_arguments() {
630		let match_args = MatchArguments {
631			functions: Some(vec![MatchParamsMap {
632				signature: "transfer(address,uint256)".to_string(),
633				args: Some(vec![
634					MatchParamEntry {
635						name: "to".to_string(),
636						value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
637							.to_string(),
638						kind: "Address".to_string(),
639						indexed: false,
640					},
641					MatchParamEntry {
642						name: "amount".to_string(),
643						value: "1000000".to_string(),
644						kind: "U64".to_string(),
645						indexed: false,
646					},
647				]),
648			}]),
649			events: Some(vec![MatchParamsMap {
650				signature: "Transfer(address,address,uint256)".to_string(),
651				args: Some(vec![
652					MatchParamEntry {
653						name: "from".to_string(),
654						value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
655							.to_string(),
656						kind: "Address".to_string(),
657						indexed: true,
658					},
659					MatchParamEntry {
660						name: "to".to_string(),
661						value: "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON"
662							.to_string(),
663						kind: "Address".to_string(),
664						indexed: true,
665					},
666					MatchParamEntry {
667						name: "amount".to_string(),
668						value: "1000000".to_string(),
669						kind: "U64".to_string(),
670						indexed: false,
671					},
672				]),
673			}]),
674		};
675
676		assert!(match_args.functions.is_some());
677		let functions = match_args.functions.unwrap();
678		assert_eq!(functions.len(), 1);
679		assert_eq!(functions[0].signature, "transfer(address,uint256)");
680
681		let function_args = functions[0].args.as_ref().unwrap();
682		assert_eq!(function_args.len(), 2);
683		assert_eq!(function_args[0].name, "to");
684		assert_eq!(function_args[0].kind, "Address");
685		assert_eq!(function_args[1].name, "amount");
686		assert_eq!(function_args[1].kind, "U64");
687
688		assert!(match_args.events.is_some());
689		let events = match_args.events.unwrap();
690		assert_eq!(events.len(), 1);
691		assert_eq!(events[0].signature, "Transfer(address,address,uint256)");
692
693		let event_args = events[0].args.as_ref().unwrap();
694		assert_eq!(event_args.len(), 3);
695		assert_eq!(event_args[0].name, "from");
696		assert!(event_args[0].indexed);
697		assert_eq!(event_args[1].name, "to");
698		assert!(event_args[1].indexed);
699		assert_eq!(event_args[2].name, "amount");
700		assert!(!event_args[2].indexed);
701	}
702
703	#[test]
704	fn test_contract_spec_deref() {
705		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
706			name: "transfer".try_into().unwrap(),
707			inputs: vec![].try_into().unwrap(),
708			outputs: vec![].try_into().unwrap(),
709			doc: "Test function documentation".try_into().unwrap(),
710		})];
711
712		let contract_spec = ContractSpec(spec_entries.clone());
713		assert_eq!(contract_spec.len(), 1);
714		if let ScSpecEntry::FunctionV0(func) = &contract_spec[0] {
715			assert_eq!(func.name.to_string(), "transfer");
716		} else {
717			panic!("Expected FunctionV0 entry");
718		}
719	}
720
721	#[test]
722	fn test_contract_spec_from_models() {
723		let json_value = serde_json::json!([
724				{
725					"function_v0": {
726						"doc": "",
727						"name": "transfer",
728						"inputs": [
729							{
730								"doc": "",
731								"name": "from",
732								"type_": "address"
733							},
734							{
735								"doc": "",
736								"name": "to",
737								"type_": "address"
738							},
739							{
740								"doc": "",
741								"name": "amount",
742								"type_": "i128"
743							}
744						],
745						"outputs": []
746					}
747				},
748			]
749		);
750
751		let stellar_spec = ContractSpec::from(json_value.clone());
752		let models_spec = ModelsContractSpec::Stellar(stellar_spec);
753		let converted_spec = ContractSpec::from(models_spec);
754		let formatted_spec = FormattedContractSpec::from(converted_spec);
755
756		assert!(!formatted_spec.functions.is_empty());
757		assert_eq!(formatted_spec.functions[0].name, "transfer");
758		assert_eq!(formatted_spec.functions[0].inputs.len(), 3);
759		assert_eq!(formatted_spec.functions[0].inputs[0].name, "from");
760		assert_eq!(formatted_spec.functions[0].inputs[0].kind, "Address");
761		assert_eq!(formatted_spec.functions[0].inputs[1].name, "to");
762		assert_eq!(formatted_spec.functions[0].inputs[1].kind, "Address");
763		assert_eq!(formatted_spec.functions[0].inputs[2].name, "amount");
764		assert_eq!(formatted_spec.functions[0].inputs[2].kind, "I128");
765
766		let evm_spec = EVMContractSpec::from(json!({}));
767		let models_spec = ModelsContractSpec::EVM(evm_spec);
768		let converted_spec = ContractSpec::from(models_spec);
769		assert!(converted_spec.is_empty());
770	}
771}