1use crate::{
2 models::rpc::{
3 SolanaFeeEstimateResult, SolanaPrepareTransactionResult, StellarFeeEstimateResult,
4 StellarPrepareTransactionResult,
5 },
6 models::{
7 evm::Speed, EvmTransactionDataSignature, NetworkTransactionData, SolanaInstructionSpec,
8 TransactionRepoModel, TransactionStatus, U256,
9 },
10 utils::{
11 deserialize_i64, deserialize_optional_u128, deserialize_optional_u64, serialize_i64,
12 serialize_optional_u128,
13 },
14};
15use serde::{Deserialize, Serialize};
16use utoipa::ToSchema;
17
18#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
19#[serde(untagged)]
20pub enum TransactionResponse {
21 Evm(Box<EvmTransactionResponse>),
22 Solana(Box<SolanaTransactionResponse>),
23 Stellar(Box<StellarTransactionResponse>),
24}
25
26#[derive(Debug, Serialize, Clone, PartialEq, Deserialize, ToSchema)]
27pub struct EvmTransactionResponse {
28 pub id: String,
29 #[schema(nullable = false)]
30 pub hash: Option<String>,
31 pub status: TransactionStatus,
32 pub status_reason: Option<String>,
33 pub created_at: String,
34 #[schema(nullable = false)]
35 pub sent_at: Option<String>,
36 #[schema(nullable = false)]
37 pub confirmed_at: Option<String>,
38 #[serde(
39 serialize_with = "serialize_optional_u128",
40 deserialize_with = "deserialize_optional_u128",
41 default
42 )]
43 #[schema(nullable = false, value_type = String)]
44 pub gas_price: Option<u128>,
45 #[serde(deserialize_with = "deserialize_optional_u64", default)]
46 pub gas_limit: Option<u64>,
47 #[serde(deserialize_with = "deserialize_optional_u64", default)]
48 #[schema(nullable = false)]
49 pub nonce: Option<u64>,
50 #[schema(value_type = String)]
51 pub value: U256,
52 pub from: String,
53 #[schema(nullable = false)]
54 pub to: Option<String>,
55 pub relayer_id: String,
56 #[schema(nullable = false)]
57 pub data: Option<String>,
58 #[serde(
59 serialize_with = "serialize_optional_u128",
60 deserialize_with = "deserialize_optional_u128",
61 default
62 )]
63 #[schema(nullable = false, value_type = String)]
64 pub max_fee_per_gas: Option<u128>,
65 #[serde(
66 serialize_with = "serialize_optional_u128",
67 deserialize_with = "deserialize_optional_u128",
68 default
69 )]
70 #[schema(nullable = false, value_type = String)]
71 pub max_priority_fee_per_gas: Option<u128>,
72 pub signature: Option<EvmTransactionDataSignature>,
73 pub speed: Option<Speed>,
74}
75
76#[derive(Debug, Serialize, Clone, PartialEq, Deserialize, ToSchema)]
77pub struct SolanaTransactionResponse {
78 pub id: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 #[schema(nullable = false)]
81 pub signature: Option<String>,
82 pub status: TransactionStatus,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 #[schema(nullable = false)]
85 pub status_reason: Option<String>,
86 pub created_at: String,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 #[schema(nullable = false)]
89 pub sent_at: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 #[schema(nullable = false)]
92 pub confirmed_at: Option<String>,
93 pub transaction: String,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 #[schema(nullable = false)]
96 pub instructions: Option<Vec<SolanaInstructionSpec>>,
97}
98
99#[derive(Debug, Serialize, Clone, PartialEq, Deserialize, ToSchema)]
100pub struct StellarTransactionResponse {
101 pub id: String,
102 #[schema(nullable = false)]
103 pub hash: Option<String>,
104 pub status: TransactionStatus,
105 pub status_reason: Option<String>,
106 pub created_at: String,
107 #[schema(nullable = false)]
108 pub sent_at: Option<String>,
109 #[schema(nullable = false)]
110 pub confirmed_at: Option<String>,
111 pub source_account: String,
112 pub fee: u32,
113 #[serde(serialize_with = "serialize_i64", deserialize_with = "deserialize_i64")]
115 #[schema(value_type = String, pattern = "^-?[0-9]+$")]
116 pub sequence_number: i64,
117 pub relayer_id: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 #[schema(nullable = false)]
120 pub transaction_result_xdr: Option<String>,
121}
122
123impl From<TransactionRepoModel> for TransactionResponse {
124 fn from(model: TransactionRepoModel) -> Self {
125 match model.network_data {
126 NetworkTransactionData::Evm(evm_data) => {
127 TransactionResponse::Evm(Box::new(EvmTransactionResponse {
128 id: model.id,
129 hash: evm_data.hash,
130 status: model.status,
131 status_reason: model.status_reason,
132 created_at: model.created_at,
133 sent_at: model.sent_at,
134 confirmed_at: model.confirmed_at,
135 gas_price: evm_data.gas_price,
136 gas_limit: evm_data.gas_limit,
137 nonce: evm_data.nonce,
138 value: evm_data.value,
139 from: evm_data.from,
140 to: evm_data.to,
141 relayer_id: model.relayer_id,
142 data: evm_data.data,
143 max_fee_per_gas: evm_data.max_fee_per_gas,
144 max_priority_fee_per_gas: evm_data.max_priority_fee_per_gas,
145 signature: evm_data.signature,
146 speed: evm_data.speed,
147 }))
148 }
149 NetworkTransactionData::Solana(solana_data) => {
150 TransactionResponse::Solana(Box::new(SolanaTransactionResponse {
151 id: model.id,
152 transaction: solana_data.transaction.unwrap_or_default(),
153 status: model.status,
154 status_reason: model.status_reason,
155 created_at: model.created_at,
156 sent_at: model.sent_at,
157 confirmed_at: model.confirmed_at,
158 signature: solana_data.signature,
159 instructions: solana_data.instructions,
160 }))
161 }
162 NetworkTransactionData::Stellar(stellar_data) => {
163 TransactionResponse::Stellar(Box::new(StellarTransactionResponse {
164 id: model.id,
165 hash: stellar_data.hash,
166 status: model.status,
167 status_reason: model.status_reason,
168 created_at: model.created_at,
169 sent_at: model.sent_at,
170 confirmed_at: model.confirmed_at,
171 source_account: stellar_data.source_account,
172 fee: stellar_data.fee.unwrap_or(0),
173 sequence_number: stellar_data.sequence_number.unwrap_or(0),
174 relayer_id: model.relayer_id,
175 transaction_result_xdr: stellar_data.transaction_result_xdr,
176 }))
177 }
178 }
179 }
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
185#[serde(untagged)]
186#[schema(as = SponsoredTransactionQuoteResponse)]
187pub enum SponsoredTransactionQuoteResponse {
188 Solana(SolanaFeeEstimateResult),
190 Stellar(StellarFeeEstimateResult),
192}
193
194#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
197#[serde(untagged)]
198#[schema(as = SponsoredTransactionBuildResponse)]
199pub enum SponsoredTransactionBuildResponse {
200 Solana(SolanaPrepareTransactionResult),
202 Stellar(StellarPrepareTransactionResult),
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::models::{
211 EvmTransactionData, NetworkType, SolanaTransactionData, StellarTransactionData,
212 TransactionRepoModel,
213 };
214 use chrono::Utc;
215
216 #[test]
217 fn test_from_transaction_repo_model_evm() {
218 let now = Utc::now().to_rfc3339();
219 let model = TransactionRepoModel {
220 id: "tx123".to_string(),
221 status: TransactionStatus::Pending,
222 status_reason: None,
223 created_at: now.clone(),
224 sent_at: Some(now.clone()),
225 confirmed_at: None,
226 relayer_id: "relayer1".to_string(),
227 priced_at: None,
228 hashes: vec![],
229 network_data: NetworkTransactionData::Evm(EvmTransactionData {
230 hash: Some("0xabc123".to_string()),
231 gas_price: Some(20_000_000_000),
232 gas_limit: Some(21000),
233 nonce: Some(5),
234 value: U256::from(1000000000000000000u128), from: "0xsender".to_string(),
236 to: Some("0xrecipient".to_string()),
237 data: None,
238 chain_id: 1,
239 signature: None,
240 speed: None,
241 max_fee_per_gas: None,
242 max_priority_fee_per_gas: None,
243 raw: None,
244 }),
245 valid_until: None,
246 network_type: NetworkType::Evm,
247 noop_count: None,
248 is_canceled: Some(false),
249 delete_at: None,
250 metadata: None,
251 };
252
253 let response = TransactionResponse::from(model.clone());
254
255 match response {
256 TransactionResponse::Evm(evm) => {
257 assert_eq!(evm.id, model.id);
258 assert_eq!(evm.hash, Some("0xabc123".to_string()));
259 assert_eq!(evm.status, TransactionStatus::Pending);
260 assert_eq!(evm.created_at, now);
261 assert_eq!(evm.sent_at, Some(now.clone()));
262 assert_eq!(evm.confirmed_at, None);
263 assert_eq!(evm.gas_price, Some(20_000_000_000));
264 assert_eq!(evm.gas_limit, Some(21000));
265 assert_eq!(evm.nonce, Some(5));
266 assert_eq!(evm.value, U256::from(1000000000000000000u128));
267 assert_eq!(evm.from, "0xsender");
268 assert_eq!(evm.to, Some("0xrecipient".to_string()));
269 assert_eq!(evm.relayer_id, "relayer1");
270 }
271 _ => panic!("Expected EvmTransactionResponse"),
272 }
273 }
274
275 #[test]
276 fn test_from_transaction_repo_model_solana() {
277 let now = Utc::now().to_rfc3339();
278 let model = TransactionRepoModel {
279 id: "tx456".to_string(),
280 status: TransactionStatus::Confirmed,
281 status_reason: None,
282 created_at: now.clone(),
283 sent_at: Some(now.clone()),
284 confirmed_at: Some(now.clone()),
285 relayer_id: "relayer2".to_string(),
286 priced_at: None,
287 hashes: vec![],
288 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
289 transaction: Some("transaction_123".to_string()),
290 instructions: None,
291 signature: Some("signature_123".to_string()),
292 }),
293 valid_until: None,
294 network_type: NetworkType::Solana,
295 noop_count: None,
296 is_canceled: Some(false),
297 delete_at: None,
298 metadata: None,
299 };
300
301 let response = TransactionResponse::from(model.clone());
302
303 match response {
304 TransactionResponse::Solana(solana) => {
305 assert_eq!(solana.id, model.id);
306 assert_eq!(solana.status, TransactionStatus::Confirmed);
307 assert_eq!(solana.created_at, now);
308 assert_eq!(solana.sent_at, Some(now.clone()));
309 assert_eq!(solana.confirmed_at, Some(now.clone()));
310 assert_eq!(solana.transaction, "transaction_123");
311 assert_eq!(solana.signature, Some("signature_123".to_string()));
312 }
313 _ => panic!("Expected SolanaTransactionResponse"),
314 }
315 }
316
317 #[test]
318 fn test_from_transaction_repo_model_stellar() {
319 let now = Utc::now().to_rfc3339();
320 let model = TransactionRepoModel {
321 id: "tx789".to_string(),
322 status: TransactionStatus::Failed,
323 status_reason: None,
324 created_at: now.clone(),
325 sent_at: Some(now.clone()),
326 confirmed_at: Some(now.clone()),
327 relayer_id: "relayer3".to_string(),
328 priced_at: None,
329 hashes: vec![],
330 network_data: NetworkTransactionData::Stellar(StellarTransactionData {
331 hash: Some("stellar_hash_123".to_string()),
332 source_account: "source_account_id".to_string(),
333 fee: Some(100),
334 sequence_number: Some(12345),
335 transaction_input: crate::models::TransactionInput::Operations(vec![]),
336 network_passphrase: "Test SDF Network ; September 2015".to_string(),
337 memo: None,
338 valid_until: None,
339 signatures: Vec::new(),
340 simulation_transaction_data: None,
341 signed_envelope_xdr: None,
342 transaction_result_xdr: None,
343 }),
344 valid_until: None,
345 network_type: NetworkType::Stellar,
346 noop_count: None,
347 is_canceled: Some(false),
348 delete_at: None,
349 metadata: None,
350 };
351
352 let response = TransactionResponse::from(model.clone());
353
354 match response {
355 TransactionResponse::Stellar(stellar) => {
356 assert_eq!(stellar.id, model.id);
357 assert_eq!(stellar.hash, Some("stellar_hash_123".to_string()));
358 assert_eq!(stellar.status, TransactionStatus::Failed);
359 assert_eq!(stellar.created_at, now);
360 assert_eq!(stellar.sent_at, Some(now.clone()));
361 assert_eq!(stellar.confirmed_at, Some(now.clone()));
362 assert_eq!(stellar.source_account, "source_account_id");
363 assert_eq!(stellar.fee, 100);
364 assert_eq!(stellar.sequence_number, 12345);
365 assert_eq!(stellar.relayer_id, "relayer3");
366 }
367 _ => panic!("Expected StellarTransactionResponse"),
368 }
369 }
370
371 #[test]
372 fn test_stellar_fee_bump_transaction_response() {
373 let now = Utc::now().to_rfc3339();
374 let model = TransactionRepoModel {
375 id: "tx-fee-bump".to_string(),
376 status: TransactionStatus::Confirmed,
377 status_reason: None,
378 created_at: now.clone(),
379 sent_at: Some(now.clone()),
380 confirmed_at: Some(now.clone()),
381 relayer_id: "relayer3".to_string(),
382 priced_at: None,
383 hashes: vec!["fee_bump_hash_456".to_string()],
384 network_data: NetworkTransactionData::Stellar(StellarTransactionData {
385 hash: Some("fee_bump_hash_456".to_string()),
386 source_account: "fee_source_account".to_string(),
387 fee: Some(200),
388 sequence_number: Some(54321),
389 transaction_input: crate::models::TransactionInput::SignedXdr {
390 xdr: "dummy_xdr".to_string(),
391 max_fee: 1_000_000,
392 },
393 network_passphrase: "Test SDF Network ; September 2015".to_string(),
394 memo: None,
395 valid_until: None,
396 signatures: Vec::new(),
397 simulation_transaction_data: None,
398 signed_envelope_xdr: None,
399 transaction_result_xdr: None,
400 }),
401 valid_until: None,
402 network_type: NetworkType::Stellar,
403 noop_count: None,
404 is_canceled: Some(false),
405 delete_at: None,
406 metadata: None,
407 };
408
409 let response = TransactionResponse::from(model.clone());
410
411 match response {
412 TransactionResponse::Stellar(stellar) => {
413 assert_eq!(stellar.id, model.id);
414 assert_eq!(stellar.hash, Some("fee_bump_hash_456".to_string()));
415 assert_eq!(stellar.status, TransactionStatus::Confirmed);
416 assert_eq!(stellar.created_at, now);
417 assert_eq!(stellar.sent_at, Some(now.clone()));
418 assert_eq!(stellar.confirmed_at, Some(now.clone()));
419 assert_eq!(stellar.source_account, "fee_source_account");
420 assert_eq!(stellar.fee, 200);
421 assert_eq!(stellar.sequence_number, 54321);
422 assert_eq!(stellar.relayer_id, "relayer3");
423 }
424 _ => panic!("Expected StellarTransactionResponse"),
425 }
426 }
427
428 #[test]
429 fn test_stellar_sequence_number_serializes_as_string() {
430 let response = StellarTransactionResponse {
431 id: "tx123".to_string(),
432 hash: None,
433 status: TransactionStatus::Confirmed,
434 status_reason: None,
435 created_at: "2024-01-01T00:00:00Z".to_string(),
436 sent_at: None,
437 confirmed_at: None,
438 source_account: "source_account_id".to_string(),
439 fee: 100,
440 sequence_number: 643918676885760,
441 relayer_id: "relayer3".to_string(),
442 transaction_result_xdr: None,
443 };
444
445 let json = serde_json::to_string(&response).unwrap();
446 assert!(
447 json.contains(r#""sequence_number":"643918676885760""#),
448 "sequence_number should serialize as a string, got: {json}"
449 );
450
451 let decoded: StellarTransactionResponse = serde_json::from_str(&json).unwrap();
452 assert_eq!(decoded.sequence_number, 643918676885760);
453 }
454
455 #[test]
456 fn test_solana_default_recent_blockhash() {
457 let now = Utc::now().to_rfc3339();
458 let model = TransactionRepoModel {
459 id: "tx456".to_string(),
460 status: TransactionStatus::Pending,
461 status_reason: None,
462 created_at: now.clone(),
463 sent_at: None,
464 confirmed_at: None,
465 relayer_id: "relayer2".to_string(),
466 priced_at: None,
467 hashes: vec![],
468 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
469 transaction: Some("transaction_123".to_string()),
470 instructions: None,
471 signature: None,
472 }),
473 valid_until: None,
474 network_type: NetworkType::Solana,
475 noop_count: None,
476 is_canceled: Some(false),
477 delete_at: None,
478 metadata: None,
479 };
480
481 let response = TransactionResponse::from(model);
482
483 match response {
484 TransactionResponse::Solana(solana) => {
485 assert_eq!(solana.transaction, "transaction_123");
486 assert_eq!(solana.signature, None);
487 }
488 _ => panic!("Expected SolanaTransactionResponse"),
489 }
490 }
491}