openzeppelin_relayer/domain/transaction/stellar/
validation.rs1use crate::models::{MemoSpec, OperationSpec, StellarValidationError, TransactionError};
7
8pub fn validate_operations(ops: &[OperationSpec]) -> Result<(), TransactionError> {
10 if ops.is_empty() {
12 return Err(StellarValidationError::EmptyOperations.into());
13 }
14
15 if ops.len() > 100 {
16 return Err(StellarValidationError::TooManyOperations {
17 count: ops.len(),
18 max: 100,
19 }
20 .into());
21 }
22
23 validate_soroban_exclusivity(ops)?;
25
26 Ok(())
27}
28
29fn validate_soroban_exclusivity(ops: &[OperationSpec]) -> Result<(), TransactionError> {
31 let soroban_ops = ops.iter().filter(|op| is_soroban_operation(op)).count();
32
33 if soroban_ops > 1 {
34 return Err(StellarValidationError::MultipleSorobanOperations.into());
35 }
36
37 if soroban_ops == 1 && ops.len() > 1 {
38 return Err(StellarValidationError::SorobanNotExclusive.into());
39 }
40
41 Ok(())
42}
43
44fn is_soroban_operation(op: &OperationSpec) -> bool {
46 matches!(
47 op,
48 OperationSpec::InvokeContract { .. }
49 | OperationSpec::CreateContract { .. }
50 | OperationSpec::UploadWasm { .. }
51 )
52}
53
54pub fn validate_soroban_memo_restriction(
56 ops: &[OperationSpec],
57 memo: &Option<MemoSpec>,
58) -> Result<(), TransactionError> {
59 let has_soroban = ops.iter().any(is_soroban_operation);
60
61 if has_soroban && memo.is_some() && !matches!(memo, Some(MemoSpec::None)) {
62 return Err(StellarValidationError::SorobanWithMemo.into());
63 }
64
65 Ok(())
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::models::AssetSpec;
72
73 #[test]
74 fn test_empty_operations_rejected() {
75 let result = validate_operations(&[]);
76 assert!(result.is_err());
77 assert!(result
78 .unwrap_err()
79 .to_string()
80 .contains("at least one operation"));
81 }
82
83 #[test]
84 fn test_too_many_operations_rejected() {
85 let ops = vec![
86 OperationSpec::Payment {
87 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
88 amount: 1000,
89 asset: AssetSpec::Native,
90 };
91 101
92 ];
93 let result = validate_operations(&ops);
94 assert!(result.is_err());
95 assert!(result
96 .unwrap_err()
97 .to_string()
98 .contains("maximum allowed is 100"));
99 }
100
101 #[test]
102 fn test_soroban_exclusivity_enforced() {
103 let ops = vec![
105 OperationSpec::InvokeContract {
106 contract_address: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
107 .to_string(),
108 function_name: "test".to_string(),
109 args: vec![],
110 auth: None,
111 },
112 OperationSpec::CreateContract {
113 source: crate::models::ContractSource::Address {
114 address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
115 },
116 wasm_hash: "abc123".to_string(),
117 salt: None,
118 constructor_args: None,
119 auth: None,
120 },
121 ];
122 let result = validate_operations(&ops);
123 assert!(result.is_err());
124
125 let ops = vec![
127 OperationSpec::InvokeContract {
128 contract_address: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
129 .to_string(),
130 function_name: "test".to_string(),
131 args: vec![],
132 auth: None,
133 },
134 OperationSpec::Payment {
135 destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
136 amount: 1000,
137 asset: AssetSpec::Native,
138 },
139 ];
140 let result = validate_operations(&ops);
141 assert!(result.is_err());
142 assert!(result
143 .unwrap_err()
144 .to_string()
145 .contains("Soroban operations must be exclusive"));
146 }
147
148 #[test]
149 fn test_soroban_memo_restriction() {
150 let soroban_op = vec![OperationSpec::InvokeContract {
151 contract_address: "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
152 .to_string(),
153 function_name: "test".to_string(),
154 args: vec![],
155 auth: None,
156 }];
157
158 let result = validate_soroban_memo_restriction(
160 &soroban_op,
161 &Some(MemoSpec::Text {
162 value: "test".to_string(),
163 }),
164 );
165 assert!(result.is_err());
166
167 let result = validate_soroban_memo_restriction(&soroban_op, &Some(MemoSpec::None));
169 assert!(result.is_ok());
170
171 let result = validate_soroban_memo_restriction(&soroban_op, &None);
173 assert!(result.is_ok());
174 }
175}