openzeppelin_relayer/models/transaction/request/
stellar.rs1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4use crate::models::transaction::stellar::{MemoSpec, OperationSpec};
5
6#[derive(Deserialize, Serialize, ToSchema)]
7pub struct StellarTransactionRequest {
8 #[schema(nullable = true)]
9 pub source_account: Option<String>,
10 pub network: String,
11 #[schema(max_length = 100, nullable = true)]
12 pub operations: Option<Vec<OperationSpec>>,
13 #[schema(nullable = true)]
14 pub memo: Option<MemoSpec>,
15 #[schema(nullable = true)]
16 pub valid_until: Option<String>,
17 #[schema(nullable = true)]
20 pub transaction_xdr: Option<String>,
21 #[schema(nullable = true)]
24 pub fee_bump: Option<bool>,
25 #[schema(nullable = true)]
27 pub max_fee: Option<i64>,
28}
29
30impl StellarTransactionRequest {
31 pub fn validate(&self) -> Result<(), crate::models::ApiError> {
36 use crate::models::ApiError;
37
38 let has_operations = self
40 .operations
41 .as_ref()
42 .map(|ops| !ops.is_empty())
43 .unwrap_or(false);
44 let has_xdr = self.transaction_xdr.is_some();
45
46 match (has_operations, has_xdr) {
47 (true, true) => {
48 return Err(ApiError::BadRequest(
49 "Cannot provide both operations and transaction_xdr".to_string(),
50 ));
51 }
52 (false, false) => {
53 return Err(ApiError::BadRequest(
54 "Must provide either operations or transaction_xdr".to_string(),
55 ));
56 }
57 _ => {}
58 }
59
60 if self.fee_bump == Some(true) && has_operations {
62 return Err(ApiError::BadRequest(
63 "Cannot request fee_bump with operations mode".to_string(),
64 ));
65 }
66
67 Ok(())
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use serde_json;
75
76 #[test]
77 fn test_serde_operations_mode() {
78 let json = r#"{
79 "source_account": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
80 "network": "testnet",
81 "operations": []
82 }"#;
83
84 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
85 assert_eq!(
86 req.source_account,
87 Some("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string())
88 );
89 assert_eq!(req.operations.as_ref().map(|ops| ops.len()), Some(0));
90 assert_eq!(req.network, "testnet");
91 }
92
93 #[test]
94 fn test_validate_operations_and_xdr() {
95 let req = StellarTransactionRequest {
96 source_account: Some(
97 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
98 ),
99 network: "testnet".to_string(),
100 operations: Some(vec![OperationSpec::Payment {
101 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
102 amount: 1000000,
103 asset: crate::models::transaction::stellar::AssetSpec::Native,
104 }]),
105 memo: None,
106 valid_until: None,
107 transaction_xdr: Some("AAAAA...".to_string()),
108 fee_bump: None,
109 max_fee: None,
110 };
111
112 let result = req.validate();
113 assert!(result.is_err());
114 assert!(result
115 .unwrap_err()
116 .to_string()
117 .contains("Cannot provide both"));
118 }
119
120 #[test]
121 fn test_validate_neither_operations_nor_xdr() {
122 let req = StellarTransactionRequest {
123 source_account: Some(
124 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
125 ),
126 network: "testnet".to_string(),
127 operations: Some(vec![]),
128 memo: None,
129 valid_until: None,
130 transaction_xdr: None,
131 fee_bump: None,
132 max_fee: None,
133 };
134
135 let result = req.validate();
136 assert!(result.is_err());
137 assert!(result
138 .unwrap_err()
139 .to_string()
140 .contains("Must provide either"));
141 }
142
143 #[test]
144 fn test_validate_fee_bump_with_operations() {
145 let req = StellarTransactionRequest {
146 source_account: Some(
147 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
148 ),
149 network: "testnet".to_string(),
150 operations: Some(vec![OperationSpec::Payment {
151 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
152 amount: 1000000,
153 asset: crate::models::transaction::stellar::AssetSpec::Native,
154 }]),
155 memo: None,
156 valid_until: None,
157 transaction_xdr: None,
158 fee_bump: Some(true),
159 max_fee: None,
160 };
161
162 let result = req.validate();
163 assert!(result.is_err());
164 assert!(result
165 .unwrap_err()
166 .to_string()
167 .contains("Cannot request fee_bump with operations"));
168 }
169
170 #[test]
171 fn test_validate_fee_bump_with_xdr() {
172 let req = StellarTransactionRequest {
173 source_account: Some(
174 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
175 ),
176 network: "testnet".to_string(),
177 operations: None,
178 memo: None,
179 valid_until: None,
180 transaction_xdr: Some("AAAAA...".to_string()),
181 fee_bump: Some(true),
182 max_fee: Some(10000000),
183 };
184
185 let result = req.validate();
186 assert!(result.is_ok());
187 }
188
189 #[test]
190 fn test_validate_valid_operations_mode() {
191 let req = StellarTransactionRequest {
192 source_account: Some(
193 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
194 ),
195 network: "testnet".to_string(),
196 operations: Some(vec![OperationSpec::Payment {
197 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
198 amount: 1000000,
199 asset: crate::models::transaction::stellar::AssetSpec::Native,
200 }]),
201 memo: None,
202 valid_until: None,
203 transaction_xdr: None,
204 fee_bump: None,
205 max_fee: None,
206 };
207
208 let result = req.validate();
209 assert!(result.is_ok());
210 }
211
212 #[test]
213 fn test_validate_valid_xdr_mode() {
214 let req = StellarTransactionRequest {
215 source_account: Some(
216 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
217 ),
218 network: "testnet".to_string(),
219 operations: None,
220 memo: None,
221 valid_until: None,
222 transaction_xdr: Some("AAAAA...".to_string()),
223 fee_bump: None,
224 max_fee: None,
225 };
226
227 let result = req.validate();
228 assert!(result.is_ok());
229 }
230
231 #[test]
232 fn test_default_structure() {
233 let req = StellarTransactionRequest {
234 source_account: Some(
235 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
236 ),
237 network: "testnet".to_string(),
238 operations: Some(vec![]),
239 memo: None,
240 valid_until: None,
241 transaction_xdr: None,
242 fee_bump: None,
243 max_fee: None,
244 };
245
246 assert_eq!(
247 req.source_account,
248 Some("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string())
249 );
250 assert_eq!(req.operations.as_ref().map(|ops| ops.len()), Some(0));
251 assert_eq!(req.network, "testnet");
252 assert!(req.memo.is_none());
253 assert!(req.valid_until.is_none());
254 assert!(req.transaction_xdr.is_none());
255 assert!(req.fee_bump.is_none());
256 assert!(req.max_fee.is_none());
257 }
258
259 #[test]
260 fn test_xdr_mode() {
261 let json = r#"{
262 "source_account": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
263 "network": "testnet",
264 "operations": [],
265 "transaction_xdr": "AAAAAgAAAABjc+mbXCnvmVk4lxqVl7s0LAz5slXqmkHBg8PpH7p3DgAAAGQABpK0AAAACQAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAGN0qQBW8x3mfbwGGYndt2uq4O4sZPUrDx5HlwuQke9zAAAAAAAAAAAAAA9CAAAAAA==",
266 "fee_bump": true,
267 "max_fee": 10000000
268 }"#;
269
270 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
271 assert_eq!(
272 req.source_account,
273 Some("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string())
274 );
275 assert!(req.transaction_xdr.is_some());
276 assert_eq!(req.fee_bump, Some(true));
277 assert_eq!(req.max_fee, Some(10000000));
278 assert_eq!(
279 req.operations.as_ref().map(|ops| ops.is_empty()),
280 Some(true)
281 );
282 }
283
284 #[test]
285 fn test_operations_with_fee_bump_is_invalid() {
286 let json = r#"{
289 "source_account": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
290 "network": "testnet",
291 "operations": [],
292 "fee_bump": true
293 }"#;
294
295 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
297 assert!(req.fee_bump == Some(true));
298 assert_eq!(
299 req.operations.as_ref().map(|ops| ops.is_empty()),
300 Some(true)
301 );
302 }
303
304 #[test]
305 fn test_xdr_mode_without_operations_field() {
306 let json = r#"{
308 "network": "testnet",
309 "fee": 1,
310 "transaction_xdr": "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA="
311 }"#;
312
313 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
314 assert_eq!(req.network, "testnet");
315 assert!(req.transaction_xdr.is_some());
316 assert!(req.operations.is_none());
317
318 assert!(req.validate().is_ok());
320 }
321}