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_events, get_contract_spec_functions,
11 get_contract_spec_with_event_parameters, get_contract_spec_with_function_input_parameters,
12 },
13};
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct MonitorMatch {
18 pub monitor: Monitor,
20
21 pub transaction: StellarTransaction,
23
24 pub ledger: StellarBlock,
26
27 pub network_slug: String,
29
30 pub matched_on: MatchConditions,
32
33 pub matched_on_args: Option<MatchArguments>,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize)]
39pub struct MatchParamsMap {
40 pub signature: String,
42
43 pub args: Option<Vec<MatchParamEntry>>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct MatchParamEntry {
50 pub name: String,
52
53 pub value: String,
55
56 pub kind: String,
58
59 pub indexed: bool,
61}
62
63#[derive(Debug, Clone, Deserialize, Serialize)]
65pub struct MatchArguments {
66 pub functions: Option<Vec<MatchParamsMap>>,
68
69 pub events: Option<Vec<MatchParamsMap>>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ParsedOperationResult {
76 pub contract_address: String,
78
79 pub function_name: String,
81
82 pub function_signature: String,
84
85 pub arguments: Vec<Value>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct DecodedParamEntry {
96 pub value: String,
98
99 pub kind: String,
101
102 pub indexed: bool,
104}
105
106#[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
121impl 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
131impl 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
142impl 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
155impl std::ops::Deref for ContractSpec {
157 type Target = Vec<ScSpecEntry>;
158
159 fn deref(&self) -> &Self::Target {
160 &self.0
161 }
162}
163
164#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
173pub struct FormattedContractSpec {
174 pub functions: Vec<ContractFunction>,
176
177 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#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
212pub struct ContractFunction {
213 pub name: String,
215
216 pub inputs: Vec<ContractInput>,
218
219 pub signature: String,
221}
222
223#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
233pub struct ContractInput {
234 pub index: u32,
236
237 pub name: String,
239
240 pub kind: String,
242}
243
244#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
246pub enum EventParamLocation {
247 #[default]
249 Indexed,
250 Data,
252}
253
254#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
259pub struct ContractEventParam {
260 pub name: String,
262
263 pub kind: String,
265
266 pub location: EventParamLocation,
268}
269
270#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
276pub struct ContractEvent {
277 pub name: String,
279
280 pub prefix_topics: Vec<String>,
283
284 pub params: Vec<ContractEventParam>,
286
287 pub signature: String,
289}
290
291#[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}