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