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#[cfg(test)]
292mod tests {
293	use super::*;
294	use crate::models::EVMContractSpec;
295	use crate::models::{
296		blockchain::stellar::block::LedgerInfo as StellarLedgerInfo,
297		blockchain::stellar::transaction::TransactionInfo as StellarTransactionInfo,
298		ContractSpec as ModelsContractSpec, FunctionCondition, MatchConditions,
299	};
300	use crate::utils::tests::builders::stellar::monitor::MonitorBuilder;
301	use serde_json::json;
302	use stellar_xdr::curr::{ScSpecEntry, ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeDef};
303
304	#[test]
305	fn test_contract_spec_from_vec() {
306		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
307			name: "test_function".try_into().unwrap(),
308			inputs: vec![].try_into().unwrap(),
309			outputs: vec![].try_into().unwrap(),
310			doc: "Test function documentation".try_into().unwrap(),
311		})];
312
313		let contract_spec = ContractSpec::from(spec_entries.clone());
314		assert_eq!(contract_spec.0, spec_entries);
315	}
316
317	#[test]
318	fn test_contract_spec_from_json() {
319		let json_value = serde_json::json!([
320			{
321				"function_v0": {
322					"doc": "Test function documentation",
323					"name": "test_function",
324					"inputs": [
325						{
326							"doc": "",
327							"name": "from",
328							"type_": "address"
329						},
330						{
331							"doc": "",
332							"name": "to",
333							"type_": "address"
334						},
335						{
336							"doc": "",
337							"name": "amount",
338							"type_": "i128"
339						}
340					],
341					"outputs": []
342				}
343			},
344		]);
345
346		let contract_spec = ContractSpec::from(json_value);
347		assert!(!contract_spec.0.is_empty());
348		if let ScSpecEntry::FunctionV0(func) = &contract_spec.0[0] {
349			assert_eq!(func.name.to_string(), "test_function");
350			assert_eq!(func.doc.to_string(), "Test function documentation");
351		} else {
352			panic!("Expected FunctionV0 entry");
353		}
354	}
355
356	#[test]
357	fn test_contract_spec_from_invalid_json() {
358		let invalid_json = serde_json::json!({
359			"invalid": "data"
360		});
361
362		let contract_spec = ContractSpec::from(invalid_json);
363		assert!(contract_spec.0.is_empty());
364	}
365
366	#[test]
367	fn test_formatted_contract_spec_from_contract_spec() {
368		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
369			name: "transfer".try_into().unwrap(),
370			inputs: vec![
371				ScSpecFunctionInputV0 {
372					name: "to".try_into().unwrap(),
373					type_: ScSpecTypeDef::Address,
374					doc: "Recipient address".try_into().unwrap(),
375				},
376				ScSpecFunctionInputV0 {
377					name: "amount".try_into().unwrap(),
378					type_: ScSpecTypeDef::U64,
379					doc: "Amount to transfer".try_into().unwrap(),
380				},
381			]
382			.try_into()
383			.unwrap(),
384			outputs: vec![].try_into().unwrap(),
385			doc: "Transfer function documentation".try_into().unwrap(),
386		})];
387
388		let contract_spec = ContractSpec(spec_entries);
389		let formatted_spec = FormattedContractSpec::from(contract_spec);
390
391		assert_eq!(formatted_spec.functions.len(), 1);
392		let function = &formatted_spec.functions[0];
393		assert_eq!(function.name, "transfer");
394		assert_eq!(function.inputs.len(), 2);
395		assert_eq!(function.inputs[0].name, "to");
396		assert_eq!(function.inputs[0].kind, "Address");
397		assert_eq!(function.inputs[1].name, "amount");
398		assert_eq!(function.inputs[1].kind, "U64");
399		assert_eq!(function.signature, "transfer(Address,U64)");
400	}
401
402	#[test]
403	fn test_contract_spec_display() {
404		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
405			name: "test_function".try_into().unwrap(),
406			inputs: vec![].try_into().unwrap(),
407			outputs: vec![].try_into().unwrap(),
408			doc: "Test function documentation".try_into().unwrap(),
409		})];
410
411		let contract_spec = ContractSpec(spec_entries);
412		let display_str = format!("{}", contract_spec);
413		assert!(!display_str.is_empty());
414		assert!(display_str.contains("test_function"));
415	}
416
417	#[test]
418	fn test_contract_spec_with_multiple_functions() {
419		let spec_entries = vec![
420			ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
421				name: "transfer".try_into().unwrap(),
422				inputs: vec![
423					ScSpecFunctionInputV0 {
424						name: "to".try_into().unwrap(),
425						type_: ScSpecTypeDef::Address,
426						doc: "Recipient address".try_into().unwrap(),
427					},
428					ScSpecFunctionInputV0 {
429						name: "amount".try_into().unwrap(),
430						type_: ScSpecTypeDef::U64,
431						doc: "Amount to transfer".try_into().unwrap(),
432					},
433				]
434				.try_into()
435				.unwrap(),
436				outputs: vec![].try_into().unwrap(),
437				doc: "Transfer function".try_into().unwrap(),
438			}),
439			ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
440				name: "balance".try_into().unwrap(),
441				inputs: vec![ScSpecFunctionInputV0 {
442					name: "account".try_into().unwrap(),
443					type_: ScSpecTypeDef::Address,
444					doc: "Account to check balance for".try_into().unwrap(),
445				}]
446				.try_into()
447				.unwrap(),
448				outputs: vec![ScSpecTypeDef::U64].try_into().unwrap(),
449				doc: "Balance function".try_into().unwrap(),
450			}),
451		];
452
453		let contract_spec = ContractSpec(spec_entries);
454		let formatted_spec = FormattedContractSpec::from(contract_spec);
455
456		assert_eq!(formatted_spec.functions.len(), 2);
457
458		let transfer_fn = formatted_spec
459			.functions
460			.iter()
461			.find(|f| f.name == "transfer")
462			.expect("Transfer function not found");
463		assert_eq!(transfer_fn.signature, "transfer(Address,U64)");
464
465		let balance_fn = formatted_spec
466			.functions
467			.iter()
468			.find(|f| f.name == "balance")
469			.expect("Balance function not found");
470		assert_eq!(balance_fn.signature, "balance(Address)");
471	}
472
473	#[test]
474	fn test_monitor_match() {
475		let monitor = MonitorBuilder::new()
476			.name("TestMonitor")
477			.function("transfer(address,uint256)", None)
478			.build();
479
480		let transaction = StellarTransaction(StellarTransactionInfo {
481			status: "SUCCESS".to_string(),
482			transaction_hash: "test_hash".to_string(),
483			application_order: 1,
484			fee_bump: false,
485			envelope_xdr: Some("mock_xdr".to_string()),
486			envelope_json: Some(serde_json::json!({
487				"type": "ENVELOPE_TYPE_TX",
488				"tx": {
489					"sourceAccount": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
490					"operations": [{
491						"type": "invokeHostFunction",
492						"function": "transfer",
493						"parameters": ["GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "1000000"]
494					}]
495				}
496			})),
497			result_xdr: Some("mock_result".to_string()),
498			result_json: None,
499			result_meta_xdr: Some("mock_meta".to_string()),
500			result_meta_json: None,
501			diagnostic_events_xdr: None,
502			diagnostic_events_json: None,
503			ledger: 123,
504			ledger_close_time: 1234567890,
505			decoded: None,
506		});
507
508		let ledger = StellarBlock(StellarLedgerInfo {
509			hash: "test_ledger_hash".to_string(),
510			sequence: 123,
511			ledger_close_time: "2024-03-20T12:00:00Z".to_string(),
512			ledger_header: "mock_header".to_string(),
513			ledger_header_json: None,
514			ledger_metadata: "mock_metadata".to_string(),
515			ledger_metadata_json: None,
516		});
517
518		let match_params = MatchParamsMap {
519			signature: "transfer(address,uint256)".to_string(),
520			args: Some(vec![
521				MatchParamEntry {
522					name: "to".to_string(),
523					value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
524					kind: "Address".to_string(),
525					indexed: false,
526				},
527				MatchParamEntry {
528					name: "amount".to_string(),
529					value: "1000000".to_string(),
530					kind: "U64".to_string(),
531					indexed: false,
532				},
533			]),
534		};
535
536		let monitor_match = MonitorMatch {
537			monitor: monitor.clone(),
538			transaction: transaction.clone(),
539			ledger: ledger.clone(),
540			network_slug: "stellar_mainnet".to_string(),
541			matched_on: MatchConditions {
542				functions: vec![FunctionCondition {
543					signature: "transfer(address,uint256)".to_string(),
544					expression: None,
545				}],
546				events: vec![],
547				transactions: vec![],
548			},
549			matched_on_args: Some(MatchArguments {
550				functions: Some(vec![match_params]),
551				events: None,
552			}),
553		};
554
555		assert_eq!(monitor_match.monitor.name, "TestMonitor");
556		assert_eq!(monitor_match.transaction.transaction_hash, "test_hash");
557		assert_eq!(monitor_match.ledger.sequence, 123);
558		assert_eq!(monitor_match.network_slug, "stellar_mainnet");
559		assert_eq!(monitor_match.matched_on.functions.len(), 1);
560		assert_eq!(
561			monitor_match.matched_on.functions[0].signature,
562			"transfer(address,uint256)"
563		);
564
565		let matched_args = monitor_match.matched_on_args.unwrap();
566		let function_args = matched_args.functions.unwrap();
567		assert_eq!(function_args.len(), 1);
568		assert_eq!(function_args[0].signature, "transfer(address,uint256)");
569
570		let args = function_args[0].args.as_ref().unwrap();
571		assert_eq!(args.len(), 2);
572		assert_eq!(args[0].name, "to");
573		assert_eq!(args[0].kind, "Address");
574		assert_eq!(args[1].name, "amount");
575		assert_eq!(args[1].kind, "U64");
576	}
577
578	#[test]
579	fn test_parsed_operation_result() {
580		let result = ParsedOperationResult {
581			contract_address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
582				.to_string(),
583			function_name: "transfer".to_string(),
584			function_signature: "transfer(address,uint256)".to_string(),
585			arguments: vec![
586				serde_json::json!("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"),
587				serde_json::json!("1000000"),
588			],
589		};
590
591		assert_eq!(
592			result.contract_address,
593			"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
594		);
595		assert_eq!(result.function_name, "transfer");
596		assert_eq!(result.function_signature, "transfer(address,uint256)");
597		assert_eq!(result.arguments.len(), 2);
598		assert_eq!(
599			result.arguments[0],
600			"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
601		);
602		assert_eq!(result.arguments[1], "1000000");
603	}
604
605	#[test]
606	fn test_decoded_param_entry() {
607		let param = DecodedParamEntry {
608			value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
609			kind: "Address".to_string(),
610			indexed: false,
611		};
612
613		assert_eq!(
614			param.value,
615			"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
616		);
617		assert_eq!(param.kind, "Address");
618		assert!(!param.indexed);
619	}
620
621	#[test]
622	fn test_match_arguments() {
623		let match_args = MatchArguments {
624			functions: Some(vec![MatchParamsMap {
625				signature: "transfer(address,uint256)".to_string(),
626				args: Some(vec![
627					MatchParamEntry {
628						name: "to".to_string(),
629						value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
630							.to_string(),
631						kind: "Address".to_string(),
632						indexed: false,
633					},
634					MatchParamEntry {
635						name: "amount".to_string(),
636						value: "1000000".to_string(),
637						kind: "U64".to_string(),
638						indexed: false,
639					},
640				]),
641			}]),
642			events: Some(vec![MatchParamsMap {
643				signature: "Transfer(address,address,uint256)".to_string(),
644				args: Some(vec![
645					MatchParamEntry {
646						name: "from".to_string(),
647						value: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
648							.to_string(),
649						kind: "Address".to_string(),
650						indexed: true,
651					},
652					MatchParamEntry {
653						name: "to".to_string(),
654						value: "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON"
655							.to_string(),
656						kind: "Address".to_string(),
657						indexed: true,
658					},
659					MatchParamEntry {
660						name: "amount".to_string(),
661						value: "1000000".to_string(),
662						kind: "U64".to_string(),
663						indexed: false,
664					},
665				]),
666			}]),
667		};
668
669		assert!(match_args.functions.is_some());
670		let functions = match_args.functions.unwrap();
671		assert_eq!(functions.len(), 1);
672		assert_eq!(functions[0].signature, "transfer(address,uint256)");
673
674		let function_args = functions[0].args.as_ref().unwrap();
675		assert_eq!(function_args.len(), 2);
676		assert_eq!(function_args[0].name, "to");
677		assert_eq!(function_args[0].kind, "Address");
678		assert_eq!(function_args[1].name, "amount");
679		assert_eq!(function_args[1].kind, "U64");
680
681		assert!(match_args.events.is_some());
682		let events = match_args.events.unwrap();
683		assert_eq!(events.len(), 1);
684		assert_eq!(events[0].signature, "Transfer(address,address,uint256)");
685
686		let event_args = events[0].args.as_ref().unwrap();
687		assert_eq!(event_args.len(), 3);
688		assert_eq!(event_args[0].name, "from");
689		assert!(event_args[0].indexed);
690		assert_eq!(event_args[1].name, "to");
691		assert!(event_args[1].indexed);
692		assert_eq!(event_args[2].name, "amount");
693		assert!(!event_args[2].indexed);
694	}
695
696	#[test]
697	fn test_contract_spec_deref() {
698		let spec_entries = vec![ScSpecEntry::FunctionV0(ScSpecFunctionV0 {
699			name: "transfer".try_into().unwrap(),
700			inputs: vec![].try_into().unwrap(),
701			outputs: vec![].try_into().unwrap(),
702			doc: "Test function documentation".try_into().unwrap(),
703		})];
704
705		let contract_spec = ContractSpec(spec_entries.clone());
706		assert_eq!(contract_spec.len(), 1);
707		if let ScSpecEntry::FunctionV0(func) = &contract_spec[0] {
708			assert_eq!(func.name.to_string(), "transfer");
709		} else {
710			panic!("Expected FunctionV0 entry");
711		}
712	}
713
714	#[test]
715	fn test_contract_spec_from_models() {
716		let json_value = serde_json::json!([
717				{
718					"function_v0": {
719						"doc": "",
720						"name": "transfer",
721						"inputs": [
722							{
723								"doc": "",
724								"name": "from",
725								"type_": "address"
726							},
727							{
728								"doc": "",
729								"name": "to",
730								"type_": "address"
731							},
732							{
733								"doc": "",
734								"name": "amount",
735								"type_": "i128"
736							}
737						],
738						"outputs": []
739					}
740				},
741			]
742		);
743
744		let stellar_spec = ContractSpec::from(json_value.clone());
745		let models_spec = ModelsContractSpec::Stellar(stellar_spec);
746		let converted_spec = ContractSpec::from(models_spec);
747		let formatted_spec = FormattedContractSpec::from(converted_spec);
748
749		assert!(!formatted_spec.functions.is_empty());
750		assert_eq!(formatted_spec.functions[0].name, "transfer");
751		assert_eq!(formatted_spec.functions[0].inputs.len(), 3);
752		assert_eq!(formatted_spec.functions[0].inputs[0].name, "from");
753		assert_eq!(formatted_spec.functions[0].inputs[0].kind, "Address");
754		assert_eq!(formatted_spec.functions[0].inputs[1].name, "to");
755		assert_eq!(formatted_spec.functions[0].inputs[1].kind, "Address");
756		assert_eq!(formatted_spec.functions[0].inputs[2].name, "amount");
757		assert_eq!(formatted_spec.functions[0].inputs[2].kind, "I128");
758
759		let evm_spec = EVMContractSpec::from(json!({}));
760		let models_spec = ModelsContractSpec::EVM(evm_spec);
761		let converted_spec = ContractSpec::from(models_spec);
762		assert!(converted_spec.is_empty());
763	}
764}