1use 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#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct MonitorMatch {
17 pub monitor: Monitor,
19
20 pub transaction: StellarTransaction,
22
23 pub ledger: StellarBlock,
25
26 pub network_slug: String,
28
29 pub matched_on: MatchConditions,
31
32 pub matched_on_args: Option<MatchArguments>,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct MatchParamsMap {
39 pub signature: String,
41
42 pub args: Option<Vec<MatchParamEntry>>,
44}
45
46#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct MatchParamEntry {
49 pub name: String,
51
52 pub value: String,
54
55 pub kind: String,
57
58 pub indexed: bool,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct MatchArguments {
65 pub functions: Option<Vec<MatchParamsMap>>,
67
68 pub events: Option<Vec<MatchParamsMap>>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ParsedOperationResult {
75 pub contract_address: String,
77
78 pub function_name: String,
80
81 pub function_signature: String,
83
84 pub arguments: Vec<Value>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct DecodedParamEntry {
95 pub value: String,
97
98 pub kind: String,
100
101 pub indexed: bool,
103}
104
105#[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
120impl 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
130impl 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
141impl 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
154impl std::ops::Deref for ContractSpec {
156 type Target = Vec<ScSpecEntry>;
157
158 fn deref(&self) -> &Self::Target {
159 &self.0
160 }
161}
162
163#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
172pub struct FormattedContractSpec {
173 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#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
204pub struct ContractFunction {
205 pub name: String,
207
208 pub inputs: Vec<ContractInput>,
210
211 pub signature: String,
213}
214
215#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
225pub struct ContractInput {
226 pub index: u32,
228
229 pub name: String,
231
232 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}