1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4use crate::domain::transaction::stellar::StellarTransactionValidator;
5use crate::models::ApiError;
6use crate::{
7 domain::stellar::validation::validate_operations, models::transaction::stellar::OperationSpec,
8};
9#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
10#[serde(deny_unknown_fields)]
11#[derive(Clone)]
12#[schema(as = StellarFeeEstimateRequestParams)]
13pub struct FeeEstimateRequestParams {
14 #[schema(nullable = true)]
17 pub transaction_xdr: Option<String>,
18 #[schema(nullable = true)]
21 pub source_account: Option<String>,
22 #[schema(nullable = true)]
25 pub operations: Option<Vec<OperationSpec>>,
26 pub fee_token: String,
28}
29
30impl FeeEstimateRequestParams {
31 pub fn validate(&self) -> Result<(), crate::models::ApiError> {
35 StellarTransactionValidator::validate_fee_token_structure(&self.fee_token)
37 .map_err(|e| ApiError::BadRequest(format!("Invalid fee_token structure: {e}")))?;
38
39 let has_operations = self
41 .operations
42 .as_ref()
43 .map(|ops| !ops.is_empty())
44 .unwrap_or(false);
45 let has_xdr = self.transaction_xdr.is_some();
46
47 if has_operations {
48 validate_operations(self.operations.as_ref().unwrap())
49 .map_err(|e| ApiError::BadRequest(format!("Invalid operations: {e}")))?;
50 if self.source_account.is_none() || self.source_account.as_ref().unwrap().is_empty() {
51 return Err(ApiError::BadRequest(
52 "source_account is required when providing operations".to_string(),
53 ));
54 }
55 }
56
57 match (has_operations, has_xdr) {
58 (true, true) => {
59 return Err(ApiError::BadRequest(
60 "Cannot provide both transaction_xdr and operations".to_string(),
61 ));
62 }
63 (false, false) => {
64 return Err(ApiError::BadRequest(
65 "Must provide either transaction_xdr or operations".to_string(),
66 ));
67 }
68 _ => {}
69 }
70
71 Ok(())
72 }
73}
74
75#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ToSchema)]
76#[schema(as = StellarFeeEstimateResult)]
77pub struct FeeEstimateResult {
78 pub fee_in_token_ui: String,
80 pub fee_in_token: String,
82 pub conversion_rate: String,
84}
85
86#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
88#[serde(deny_unknown_fields)]
89#[derive(Clone)]
90#[schema(as = StellarPrepareTransactionRequestParams)]
91pub struct PrepareTransactionRequestParams {
92 #[schema(nullable = true)]
95 pub transaction_xdr: Option<String>,
96 #[schema(nullable = true)]
99 pub operations: Option<Vec<OperationSpec>>,
100 #[schema(nullable = true)]
103 pub source_account: Option<String>,
104 pub fee_token: String,
106}
107
108impl PrepareTransactionRequestParams {
109 pub fn validate(&self) -> Result<(), crate::models::ApiError> {
114 StellarTransactionValidator::validate_fee_token_structure(&self.fee_token)
116 .map_err(|e| ApiError::BadRequest(format!("Invalid fee_token structure: {e}")))?;
117
118 let has_operations = self
120 .operations
121 .as_ref()
122 .map(|ops| !ops.is_empty())
123 .unwrap_or(false);
124 let has_xdr = self.transaction_xdr.is_some();
125
126 match (has_operations, has_xdr) {
127 (true, true) => {
128 return Err(ApiError::BadRequest(
129 "Cannot provide both transaction_xdr and operations".to_string(),
130 ));
131 }
132 (false, false) => {
133 return Err(ApiError::BadRequest(
134 "Must provide either transaction_xdr or operations".to_string(),
135 ));
136 }
137 _ => {}
138 }
139
140 if has_operations {
142 validate_operations(self.operations.as_ref().unwrap())
143 .map_err(|e| ApiError::BadRequest(format!("Invalid operations: {e}")))?;
144 if self.source_account.is_none() || self.source_account.as_ref().unwrap().is_empty() {
145 return Err(ApiError::BadRequest(
146 "source_account is required when providing operations".to_string(),
147 ));
148 }
149 }
150
151 Ok(())
152 }
153}
154
155#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ToSchema)]
156#[schema(as = StellarPrepareTransactionResult)]
157pub struct PrepareTransactionResult {
158 pub transaction: String,
160 pub fee_in_token: String,
162 pub fee_in_token_ui: String,
164 pub fee_in_stroops: String,
166 pub fee_token: String,
168 pub valid_until: String,
170}
171
172pub enum StellarRpcMethod {
174 Generic(String),
175}
176
177#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Clone)]
178#[serde(untagged)]
179#[schema(as = StellarRpcRequest)]
180pub enum StellarRpcRequest {
181 #[serde(rename = "rawRpcRequest")]
182 #[schema(example = "rawRpcRequest")]
183 RawRpcRequest {
184 method: String,
185 params: serde_json::Value,
186 },
187}
188
189#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
190#[serde(untagged)]
191pub enum StellarRpcResult {
192 RawRpcResult(serde_json::Value),
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::models::transaction::stellar::{asset::AssetSpec, OperationSpec};
200
201 const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
202 const VALID_FEE_TOKEN_NATIVE: &str = "native";
203 const VALID_FEE_TOKEN_USDC: &str =
204 "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
205 const INVALID_FEE_TOKEN: &str = "invalid-token";
206
207 #[test]
210 fn test_fee_estimate_validate_with_xdr_success() {
211 let params = FeeEstimateRequestParams {
212 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
213 operations: None,
214 source_account: None,
215 fee_token: VALID_FEE_TOKEN_USDC.to_string(),
216 };
217 assert!(params.validate().is_ok());
218 }
219
220 #[test]
221 fn test_fee_estimate_validate_with_operations_success() {
222 let params = FeeEstimateRequestParams {
223 transaction_xdr: None,
224 operations: Some(vec![OperationSpec::Payment {
225 destination: TEST_PK.to_string(),
226 amount: 1000000,
227 asset: AssetSpec::Native,
228 }]),
229 source_account: Some(TEST_PK.to_string()),
230 fee_token: VALID_FEE_TOKEN_USDC.to_string(),
231 };
232 assert!(params.validate().is_ok());
233 }
234
235 #[test]
236 fn test_fee_estimate_validate_with_usdc_token_success() {
237 let params = FeeEstimateRequestParams {
238 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
239 operations: None,
240 source_account: None,
241 fee_token: VALID_FEE_TOKEN_USDC.to_string(),
242 };
243 assert!(params.validate().is_ok());
244 }
245
246 #[test]
247 fn test_fee_estimate_validate_invalid_fee_token() {
248 let params = FeeEstimateRequestParams {
249 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
250 operations: None,
251 source_account: None,
252 fee_token: INVALID_FEE_TOKEN.to_string(),
253 };
254 let result = params.validate();
255 assert!(result.is_err());
256 if let Err(ApiError::BadRequest(msg)) = result {
257 assert!(msg.contains("Invalid fee_token structure"));
258 } else {
259 panic!("Expected BadRequest error for invalid fee_token");
260 }
261 }
262
263 #[test]
264 fn test_fee_estimate_validate_both_xdr_and_operations() {
265 let params = FeeEstimateRequestParams {
266 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
267 operations: Some(vec![OperationSpec::Payment {
268 destination: TEST_PK.to_string(),
269 amount: 1000000,
270 asset: AssetSpec::Native,
271 }]),
272 source_account: Some(TEST_PK.to_string()),
273 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
274 };
275 let result = params.validate();
276 assert!(result.is_err());
277 if let Err(ApiError::BadRequest(msg)) = result {
278 assert!(msg.contains("Cannot provide both transaction_xdr and operations"));
279 } else {
280 panic!("Expected BadRequest error for both xdr and operations");
281 }
282 }
283
284 #[test]
285 fn test_fee_estimate_validate_neither_xdr_nor_operations() {
286 let params = FeeEstimateRequestParams {
287 transaction_xdr: None,
288 operations: None,
289 source_account: None,
290 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
291 };
292 let result = params.validate();
293 assert!(result.is_err());
294 if let Err(ApiError::BadRequest(msg)) = result {
295 assert!(msg.contains("Must provide either transaction_xdr or operations"));
296 } else {
297 panic!("Expected BadRequest error for missing both xdr and operations");
298 }
299 }
300
301 #[test]
302 fn test_fee_estimate_validate_operations_without_source_account() {
303 let params = FeeEstimateRequestParams {
304 transaction_xdr: None,
305 operations: Some(vec![OperationSpec::Payment {
306 destination: TEST_PK.to_string(),
307 amount: 1000000,
308 asset: AssetSpec::Native,
309 }]),
310 source_account: None,
311 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
312 };
313 let result = params.validate();
314 assert!(result.is_err());
315 if let Err(ApiError::BadRequest(msg)) = result {
316 assert!(msg.contains("source_account is required when providing operations"));
317 } else {
318 panic!("Expected BadRequest error for missing source_account");
319 }
320 }
321
322 #[test]
323 fn test_fee_estimate_validate_operations_with_empty_source_account() {
324 let params = FeeEstimateRequestParams {
325 transaction_xdr: None,
326 operations: Some(vec![OperationSpec::Payment {
327 destination: TEST_PK.to_string(),
328 amount: 1000000,
329 asset: AssetSpec::Native,
330 }]),
331 source_account: Some("".to_string()),
332 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
333 };
334 let result = params.validate();
335 assert!(result.is_err());
336 if let Err(ApiError::BadRequest(msg)) = result {
337 assert!(msg.contains("source_account is required when providing operations"));
338 } else {
339 panic!("Expected BadRequest error for empty source_account");
340 }
341 }
342
343 #[test]
344 fn test_fee_estimate_validate_empty_operations() {
345 let params = FeeEstimateRequestParams {
346 transaction_xdr: None,
347 operations: Some(vec![]),
348 source_account: Some(TEST_PK.to_string()),
349 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
350 };
351 let result = params.validate();
352 assert!(result.is_err());
353 if let Err(ApiError::BadRequest(msg)) = result {
354 assert!(msg.contains("Must provide either transaction_xdr or operations"));
355 } else {
356 panic!("Expected BadRequest error for empty operations");
357 }
358 }
359
360 #[test]
363 fn test_prepare_transaction_validate_with_xdr_success() {
364 let params = PrepareTransactionRequestParams {
365 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
366 operations: None,
367 source_account: None,
368 fee_token: VALID_FEE_TOKEN_USDC.to_string(),
369 };
370 assert!(params.validate().is_ok());
371 }
372
373 #[test]
374 fn test_prepare_transaction_validate_with_operations_success() {
375 let params = PrepareTransactionRequestParams {
376 transaction_xdr: None,
377 operations: Some(vec![OperationSpec::Payment {
378 destination: TEST_PK.to_string(),
379 amount: 1000000,
380 asset: AssetSpec::Native,
381 }]),
382 source_account: Some(TEST_PK.to_string()),
383 fee_token: VALID_FEE_TOKEN_USDC.to_string(),
384 };
385 assert!(params.validate().is_ok());
386 }
387
388 #[test]
389 fn test_prepare_transaction_validate_with_usdc_token_success() {
390 let params = PrepareTransactionRequestParams {
391 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
392 operations: None,
393 source_account: None,
394 fee_token: VALID_FEE_TOKEN_USDC.to_string(),
395 };
396 assert!(params.validate().is_ok());
397 }
398
399 #[test]
400 fn test_prepare_transaction_validate_invalid_fee_token() {
401 let params = PrepareTransactionRequestParams {
402 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
403 operations: None,
404 source_account: None,
405 fee_token: INVALID_FEE_TOKEN.to_string(),
406 };
407 let result = params.validate();
408 assert!(result.is_err());
409 if let Err(ApiError::BadRequest(msg)) = result {
410 assert!(msg.contains("Invalid fee_token structure"));
411 } else {
412 panic!("Expected BadRequest error for invalid fee_token");
413 }
414 }
415
416 #[test]
417 fn test_prepare_transaction_validate_both_xdr_and_operations() {
418 let params = PrepareTransactionRequestParams {
419 transaction_xdr: Some("AAAAAgAAAAA=".to_string()),
420 operations: Some(vec![OperationSpec::Payment {
421 destination: TEST_PK.to_string(),
422 amount: 1000000,
423 asset: AssetSpec::Native,
424 }]),
425 source_account: Some(TEST_PK.to_string()),
426 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
427 };
428 let result = params.validate();
429 assert!(result.is_err());
430 if let Err(ApiError::BadRequest(msg)) = result {
431 assert!(msg.contains("Cannot provide both transaction_xdr and operations"));
432 } else {
433 panic!("Expected BadRequest error for both xdr and operations");
434 }
435 }
436
437 #[test]
438 fn test_prepare_transaction_validate_neither_xdr_nor_operations() {
439 let params = PrepareTransactionRequestParams {
440 transaction_xdr: None,
441 operations: None,
442 source_account: None,
443 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
444 };
445 let result = params.validate();
446 assert!(result.is_err());
447 if let Err(ApiError::BadRequest(msg)) = result {
448 assert!(msg.contains("Must provide either transaction_xdr or operations"));
449 } else {
450 panic!("Expected BadRequest error for missing both xdr and operations");
451 }
452 }
453
454 #[test]
455 fn test_prepare_transaction_validate_operations_without_source_account() {
456 let params = PrepareTransactionRequestParams {
457 transaction_xdr: None,
458 operations: Some(vec![OperationSpec::Payment {
459 destination: TEST_PK.to_string(),
460 amount: 1000000,
461 asset: AssetSpec::Native,
462 }]),
463 source_account: None,
464 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
465 };
466 let result = params.validate();
467 assert!(result.is_err());
468 if let Err(ApiError::BadRequest(msg)) = result {
469 assert!(msg.contains("source_account is required when providing operations"));
470 } else {
471 panic!("Expected BadRequest error for missing source_account");
472 }
473 }
474
475 #[test]
476 fn test_prepare_transaction_validate_operations_with_empty_source_account() {
477 let params = PrepareTransactionRequestParams {
478 transaction_xdr: None,
479 operations: Some(vec![OperationSpec::Payment {
480 destination: TEST_PK.to_string(),
481 amount: 1000000,
482 asset: AssetSpec::Native,
483 }]),
484 source_account: Some("".to_string()),
485 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
486 };
487 let result = params.validate();
488 assert!(result.is_err());
489 if let Err(ApiError::BadRequest(msg)) = result {
490 assert!(msg.contains("source_account is required when providing operations"));
491 } else {
492 panic!("Expected BadRequest error for empty source_account");
493 }
494 }
495
496 #[test]
497 fn test_prepare_transaction_validate_empty_operations() {
498 let params = PrepareTransactionRequestParams {
499 transaction_xdr: None,
500 operations: Some(vec![]),
501 source_account: Some(TEST_PK.to_string()),
502 fee_token: VALID_FEE_TOKEN_NATIVE.to_string(),
503 };
504 let result = params.validate();
505 assert!(result.is_err());
506 if let Err(ApiError::BadRequest(msg)) = result {
509 assert!(msg.contains("Must provide either transaction_xdr or operations"));
510 } else {
511 panic!("Expected BadRequest error for empty operations");
512 }
513 }
514}