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