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