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)]
22 pub transaction_xdr: Option<String>,
23 #[schema(nullable = true)]
26 pub fee_bump: Option<bool>,
27 #[schema(nullable = true)]
29 pub max_fee: Option<i64>,
30 #[schema(nullable = true)]
35 pub signed_auth_entry: Option<String>,
36}
37
38impl StellarTransactionRequest {
39 pub fn validate(&self) -> Result<(), crate::models::ApiError> {
46 use crate::models::ApiError;
47
48 let has_operations = self
50 .operations
51 .as_ref()
52 .map(|ops| !ops.is_empty())
53 .unwrap_or(false);
54 let has_xdr = self.transaction_xdr.is_some();
55 let has_signed_auth_entry = self.signed_auth_entry.is_some();
56
57 match (has_operations, has_xdr) {
58 (true, true) => {
59 return Err(ApiError::BadRequest(
60 "Cannot provide both operations and transaction_xdr".to_string(),
61 ));
62 }
63 (false, false) => {
64 return Err(ApiError::BadRequest(
65 "Must provide either operations or transaction_xdr".to_string(),
66 ));
67 }
68 _ => {}
69 }
70
71 if self.fee_bump == Some(true) && has_operations {
73 return Err(ApiError::BadRequest(
74 "Cannot request fee_bump with operations mode".to_string(),
75 ));
76 }
77
78 if has_signed_auth_entry {
80 if !has_xdr {
81 return Err(ApiError::BadRequest(
82 "signed_auth_entry requires transaction_xdr to be provided".to_string(),
83 ));
84 }
85 if self.fee_bump == Some(true) {
86 return Err(ApiError::BadRequest(
87 "Cannot use both signed_auth_entry and fee_bump".to_string(),
88 ));
89 }
90 }
91
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use serde_json;
100
101 #[test]
102 fn test_serde_operations_mode() {
103 let json = r#"{
104 "source_account": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
105 "network": "testnet",
106 "operations": []
107 }"#;
108
109 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
110 assert_eq!(
111 req.source_account,
112 Some("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string())
113 );
114 assert_eq!(req.operations.as_ref().map(|ops| ops.len()), Some(0));
115 assert_eq!(req.network, "testnet");
116 }
117
118 #[test]
119 fn test_validate_operations_and_xdr() {
120 let req = StellarTransactionRequest {
121 source_account: Some(
122 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
123 ),
124 network: "testnet".to_string(),
125 operations: Some(vec![OperationSpec::Payment {
126 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
127 amount: 1000000,
128 asset: crate::models::transaction::stellar::AssetSpec::Native,
129 }]),
130 memo: None,
131 valid_until: None,
132 transaction_xdr: Some("AAAAA...".to_string()),
133 fee_bump: None,
134 max_fee: None,
135 signed_auth_entry: None,
136 };
137
138 let result = req.validate();
139 assert!(result.is_err());
140 assert!(result
141 .unwrap_err()
142 .to_string()
143 .contains("Cannot provide both"));
144 }
145
146 #[test]
147 fn test_validate_neither_operations_nor_xdr() {
148 let req = StellarTransactionRequest {
149 source_account: Some(
150 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
151 ),
152 network: "testnet".to_string(),
153 operations: Some(vec![]),
154 memo: None,
155 valid_until: None,
156 transaction_xdr: None,
157 fee_bump: None,
158 max_fee: None,
159 signed_auth_entry: None,
160 };
161
162 let result = req.validate();
163 assert!(result.is_err());
164 assert!(result
165 .unwrap_err()
166 .to_string()
167 .contains("Must provide either"));
168 }
169
170 #[test]
171 fn test_validate_fee_bump_with_operations() {
172 let req = StellarTransactionRequest {
173 source_account: Some(
174 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
175 ),
176 network: "testnet".to_string(),
177 operations: Some(vec![OperationSpec::Payment {
178 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
179 amount: 1000000,
180 asset: crate::models::transaction::stellar::AssetSpec::Native,
181 }]),
182 memo: None,
183 valid_until: None,
184 transaction_xdr: None,
185 fee_bump: Some(true),
186 max_fee: None,
187 signed_auth_entry: None,
188 };
189
190 let result = req.validate();
191 assert!(result.is_err());
192 assert!(result
193 .unwrap_err()
194 .to_string()
195 .contains("Cannot request fee_bump with operations"));
196 }
197
198 #[test]
199 fn test_validate_fee_bump_with_xdr() {
200 let req = StellarTransactionRequest {
201 source_account: Some(
202 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
203 ),
204 network: "testnet".to_string(),
205 operations: None,
206 memo: None,
207 valid_until: None,
208 transaction_xdr: Some("AAAAA...".to_string()),
209 fee_bump: Some(true),
210 max_fee: Some(10000000),
211 signed_auth_entry: None,
212 };
213
214 let result = req.validate();
215 assert!(result.is_ok());
216 }
217
218 #[test]
219 fn test_validate_valid_operations_mode() {
220 let req = StellarTransactionRequest {
221 source_account: Some(
222 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
223 ),
224 network: "testnet".to_string(),
225 operations: Some(vec![OperationSpec::Payment {
226 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
227 amount: 1000000,
228 asset: crate::models::transaction::stellar::AssetSpec::Native,
229 }]),
230 memo: None,
231 valid_until: None,
232 transaction_xdr: None,
233 fee_bump: None,
234 max_fee: None,
235 signed_auth_entry: None,
236 };
237
238 let result = req.validate();
239 assert!(result.is_ok());
240 }
241
242 #[test]
243 fn test_validate_valid_xdr_mode() {
244 let req = StellarTransactionRequest {
245 source_account: Some(
246 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
247 ),
248 network: "testnet".to_string(),
249 operations: None,
250 memo: None,
251 valid_until: None,
252 transaction_xdr: Some("AAAAA...".to_string()),
253 fee_bump: None,
254 max_fee: None,
255 signed_auth_entry: None,
256 };
257
258 let result = req.validate();
259 assert!(result.is_ok());
260 }
261
262 #[test]
263 fn test_default_structure() {
264 let req = StellarTransactionRequest {
265 source_account: Some(
266 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
267 ),
268 network: "testnet".to_string(),
269 operations: Some(vec![]),
270 memo: None,
271 valid_until: None,
272 transaction_xdr: None,
273 fee_bump: None,
274 max_fee: None,
275 signed_auth_entry: None,
276 };
277
278 assert_eq!(
279 req.source_account,
280 Some("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string())
281 );
282 assert_eq!(req.operations.as_ref().map(|ops| ops.len()), Some(0));
283 assert_eq!(req.network, "testnet");
284 assert!(req.memo.is_none());
285 assert!(req.valid_until.is_none());
286 assert!(req.transaction_xdr.is_none());
287 assert!(req.fee_bump.is_none());
288 assert!(req.max_fee.is_none());
289 assert!(req.signed_auth_entry.is_none());
290 }
291
292 #[test]
293 fn test_xdr_mode() {
294 let json = r#"{
295 "source_account": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
296 "network": "testnet",
297 "operations": [],
298 "transaction_xdr": "AAAAAgAAAABjc+mbXCnvmVk4lxqVl7s0LAz5slXqmkHBg8PpH7p3DgAAAGQABpK0AAAACQAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAGN0qQBW8x3mfbwGGYndt2uq4O4sZPUrDx5HlwuQke9zAAAAAAAAAAAAAA9CAAAAAA==",
299 "fee_bump": true,
300 "max_fee": 10000000
301 }"#;
302
303 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
304 assert_eq!(
305 req.source_account,
306 Some("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string())
307 );
308 assert!(req.transaction_xdr.is_some());
309 assert_eq!(req.fee_bump, Some(true));
310 assert_eq!(req.max_fee, Some(10000000));
311 assert_eq!(
312 req.operations.as_ref().map(|ops| ops.is_empty()),
313 Some(true)
314 );
315 }
316
317 #[test]
318 fn test_operations_with_fee_bump_is_invalid() {
319 let json = r#"{
322 "source_account": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
323 "network": "testnet",
324 "operations": [],
325 "fee_bump": true
326 }"#;
327
328 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
330 assert!(req.fee_bump == Some(true));
331 assert_eq!(
332 req.operations.as_ref().map(|ops| ops.is_empty()),
333 Some(true)
334 );
335 }
336
337 #[test]
338 fn test_xdr_mode_without_operations_field() {
339 let json = r#"{
341 "network": "testnet",
342 "fee": 1,
343 "transaction_xdr": "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA="
344 }"#;
345
346 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
347 assert_eq!(req.network, "testnet");
348 assert!(req.transaction_xdr.is_some());
349 assert!(req.operations.is_none());
350
351 assert!(req.validate().is_ok());
353 }
354
355 #[test]
356 fn test_validate_signed_auth_entry_with_xdr() {
357 let req = StellarTransactionRequest {
359 source_account: None,
360 network: "testnet".to_string(),
361 operations: None,
362 memo: None,
363 valid_until: None,
364 transaction_xdr: Some("AAAAA...".to_string()),
365 fee_bump: None,
366 max_fee: None,
367 signed_auth_entry: Some("BBBBB...".to_string()),
368 };
369
370 let result = req.validate();
371 assert!(result.is_ok());
372 }
373
374 #[test]
375 fn test_validate_signed_auth_entry_without_xdr() {
376 let req = StellarTransactionRequest {
378 source_account: None,
379 network: "testnet".to_string(),
380 operations: Some(vec![OperationSpec::Payment {
381 destination: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
382 amount: 1000000,
383 asset: crate::models::transaction::stellar::AssetSpec::Native,
384 }]),
385 memo: None,
386 valid_until: None,
387 transaction_xdr: None,
388 fee_bump: None,
389 max_fee: None,
390 signed_auth_entry: Some("BBBBB...".to_string()),
391 };
392
393 let result = req.validate();
394 assert!(result.is_err());
395 assert!(result
396 .unwrap_err()
397 .to_string()
398 .contains("signed_auth_entry requires transaction_xdr"));
399 }
400
401 #[test]
402 fn test_validate_signed_auth_entry_with_fee_bump() {
403 let req = StellarTransactionRequest {
405 source_account: None,
406 network: "testnet".to_string(),
407 operations: None,
408 memo: None,
409 valid_until: None,
410 transaction_xdr: Some("AAAAA...".to_string()),
411 fee_bump: Some(true),
412 max_fee: Some(10000000),
413 signed_auth_entry: Some("BBBBB...".to_string()),
414 };
415
416 let result = req.validate();
417 assert!(result.is_err());
418 assert!(result
419 .unwrap_err()
420 .to_string()
421 .contains("Cannot use both signed_auth_entry and fee_bump"));
422 }
423
424 #[test]
425 fn test_serde_signed_auth_entry() {
426 let json = r#"{
428 "network": "testnet",
429 "transaction_xdr": "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=",
430 "signed_auth_entry": "AAAAAQAAAAEAAAAHYm9pbGVycz..."
431 }"#;
432
433 let req: StellarTransactionRequest = serde_json::from_str(json).unwrap();
434 assert_eq!(req.network, "testnet");
435 assert!(req.transaction_xdr.is_some());
436 assert!(req.signed_auth_entry.is_some());
437 assert_eq!(
438 req.signed_auth_entry,
439 Some("AAAAAQAAAAEAAAAHYm9pbGVycz...".to_string())
440 );
441
442 assert!(req.validate().is_ok());
444 }
445}