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