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#[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}