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, value_type = String)]
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, value_type = String)]
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, value_type = String)]
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 pub relayer_id: String,
101}
102
103impl From<TransactionRepoModel> for TransactionResponse {
104 fn from(model: TransactionRepoModel) -> Self {
105 match model.network_data {
106 NetworkTransactionData::Evm(evm_data) => {
107 TransactionResponse::Evm(Box::new(EvmTransactionResponse {
108 id: model.id,
109 hash: evm_data.hash,
110 status: model.status,
111 status_reason: model.status_reason,
112 created_at: model.created_at,
113 sent_at: model.sent_at,
114 confirmed_at: model.confirmed_at,
115 gas_price: evm_data.gas_price,
116 gas_limit: evm_data.gas_limit,
117 nonce: evm_data.nonce,
118 value: evm_data.value,
119 from: evm_data.from,
120 to: evm_data.to,
121 relayer_id: model.relayer_id,
122 data: evm_data.data,
123 max_fee_per_gas: evm_data.max_fee_per_gas,
124 max_priority_fee_per_gas: evm_data.max_priority_fee_per_gas,
125 signature: evm_data.signature,
126 speed: evm_data.speed,
127 }))
128 }
129 NetworkTransactionData::Solana(solana_data) => {
130 TransactionResponse::Solana(Box::new(SolanaTransactionResponse {
131 id: model.id,
132 transaction: solana_data.transaction,
133 status: model.status,
134 status_reason: model.status_reason,
135 created_at: model.created_at,
136 sent_at: model.sent_at,
137 confirmed_at: model.confirmed_at,
138 signature: solana_data.signature,
139 }))
140 }
141 NetworkTransactionData::Stellar(stellar_data) => {
142 TransactionResponse::Stellar(Box::new(StellarTransactionResponse {
143 id: model.id,
144 hash: stellar_data.hash,
145 status: model.status,
146 status_reason: model.status_reason,
147 created_at: model.created_at,
148 sent_at: model.sent_at,
149 confirmed_at: model.confirmed_at,
150 source_account: stellar_data.source_account,
151 fee: stellar_data.fee.unwrap_or(0),
152 sequence_number: stellar_data.sequence_number.unwrap_or(0),
153 relayer_id: model.relayer_id,
154 }))
155 }
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::models::{
164 EvmTransactionData, NetworkType, SolanaTransactionData, StellarTransactionData,
165 TransactionRepoModel,
166 };
167 use chrono::Utc;
168
169 #[test]
170 fn test_from_transaction_repo_model_evm() {
171 let now = Utc::now().to_rfc3339();
172 let model = TransactionRepoModel {
173 id: "tx123".to_string(),
174 status: TransactionStatus::Pending,
175 status_reason: None,
176 created_at: now.clone(),
177 sent_at: Some(now.clone()),
178 confirmed_at: None,
179 relayer_id: "relayer1".to_string(),
180 priced_at: None,
181 hashes: vec![],
182 network_data: NetworkTransactionData::Evm(EvmTransactionData {
183 hash: Some("0xabc123".to_string()),
184 gas_price: Some(20_000_000_000),
185 gas_limit: Some(21000),
186 nonce: Some(5),
187 value: U256::from(1000000000000000000u128), from: "0xsender".to_string(),
189 to: Some("0xrecipient".to_string()),
190 data: None,
191 chain_id: 1,
192 signature: None,
193 speed: None,
194 max_fee_per_gas: None,
195 max_priority_fee_per_gas: None,
196 raw: None,
197 }),
198 valid_until: None,
199 network_type: NetworkType::Evm,
200 noop_count: None,
201 is_canceled: Some(false),
202 delete_at: None,
203 };
204
205 let response = TransactionResponse::from(model.clone());
206
207 match response {
208 TransactionResponse::Evm(evm) => {
209 assert_eq!(evm.id, model.id);
210 assert_eq!(evm.hash, Some("0xabc123".to_string()));
211 assert_eq!(evm.status, TransactionStatus::Pending);
212 assert_eq!(evm.created_at, now);
213 assert_eq!(evm.sent_at, Some(now.clone()));
214 assert_eq!(evm.confirmed_at, None);
215 assert_eq!(evm.gas_price, Some(20_000_000_000));
216 assert_eq!(evm.gas_limit, Some(21000));
217 assert_eq!(evm.nonce, Some(5));
218 assert_eq!(evm.value, U256::from(1000000000000000000u128));
219 assert_eq!(evm.from, "0xsender");
220 assert_eq!(evm.to, Some("0xrecipient".to_string()));
221 assert_eq!(evm.relayer_id, "relayer1");
222 }
223 _ => panic!("Expected EvmTransactionResponse"),
224 }
225 }
226
227 #[test]
228 fn test_from_transaction_repo_model_solana() {
229 let now = Utc::now().to_rfc3339();
230 let model = TransactionRepoModel {
231 id: "tx456".to_string(),
232 status: TransactionStatus::Confirmed,
233 status_reason: None,
234 created_at: now.clone(),
235 sent_at: Some(now.clone()),
236 confirmed_at: Some(now.clone()),
237 relayer_id: "relayer2".to_string(),
238 priced_at: None,
239 hashes: vec![],
240 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
241 transaction: "transaction_123".to_string(),
242 signature: Some("signature_123".to_string()),
243 }),
244 valid_until: None,
245 network_type: NetworkType::Solana,
246 noop_count: None,
247 is_canceled: Some(false),
248 delete_at: None,
249 };
250
251 let response = TransactionResponse::from(model.clone());
252
253 match response {
254 TransactionResponse::Solana(solana) => {
255 assert_eq!(solana.id, model.id);
256 assert_eq!(solana.status, TransactionStatus::Confirmed);
257 assert_eq!(solana.created_at, now);
258 assert_eq!(solana.sent_at, Some(now.clone()));
259 assert_eq!(solana.confirmed_at, Some(now.clone()));
260 assert_eq!(solana.transaction, "transaction_123");
261 assert_eq!(solana.signature, Some("signature_123".to_string()));
262 }
263 _ => panic!("Expected SolanaTransactionResponse"),
264 }
265 }
266
267 #[test]
268 fn test_from_transaction_repo_model_stellar() {
269 let now = Utc::now().to_rfc3339();
270 let model = TransactionRepoModel {
271 id: "tx789".to_string(),
272 status: TransactionStatus::Failed,
273 status_reason: None,
274 created_at: now.clone(),
275 sent_at: Some(now.clone()),
276 confirmed_at: Some(now.clone()),
277 relayer_id: "relayer3".to_string(),
278 priced_at: None,
279 hashes: vec![],
280 network_data: NetworkTransactionData::Stellar(StellarTransactionData {
281 hash: Some("stellar_hash_123".to_string()),
282 source_account: "source_account_id".to_string(),
283 fee: Some(100),
284 sequence_number: Some(12345),
285 transaction_input: crate::models::TransactionInput::Operations(vec![]),
286 network_passphrase: "Test SDF Network ; September 2015".to_string(),
287 memo: None,
288 valid_until: None,
289 signatures: Vec::new(),
290 simulation_transaction_data: None,
291 signed_envelope_xdr: None,
292 }),
293 valid_until: None,
294 network_type: NetworkType::Stellar,
295 noop_count: None,
296 is_canceled: Some(false),
297 delete_at: None,
298 };
299
300 let response = TransactionResponse::from(model.clone());
301
302 match response {
303 TransactionResponse::Stellar(stellar) => {
304 assert_eq!(stellar.id, model.id);
305 assert_eq!(stellar.hash, Some("stellar_hash_123".to_string()));
306 assert_eq!(stellar.status, TransactionStatus::Failed);
307 assert_eq!(stellar.created_at, now);
308 assert_eq!(stellar.sent_at, Some(now.clone()));
309 assert_eq!(stellar.confirmed_at, Some(now.clone()));
310 assert_eq!(stellar.source_account, "source_account_id");
311 assert_eq!(stellar.fee, 100);
312 assert_eq!(stellar.sequence_number, 12345);
313 assert_eq!(stellar.relayer_id, "relayer3");
314 }
315 _ => panic!("Expected StellarTransactionResponse"),
316 }
317 }
318
319 #[test]
320 fn test_stellar_fee_bump_transaction_response() {
321 let now = Utc::now().to_rfc3339();
322 let model = TransactionRepoModel {
323 id: "tx-fee-bump".to_string(),
324 status: TransactionStatus::Confirmed,
325 status_reason: None,
326 created_at: now.clone(),
327 sent_at: Some(now.clone()),
328 confirmed_at: Some(now.clone()),
329 relayer_id: "relayer3".to_string(),
330 priced_at: None,
331 hashes: vec!["fee_bump_hash_456".to_string()],
332 network_data: NetworkTransactionData::Stellar(StellarTransactionData {
333 hash: Some("fee_bump_hash_456".to_string()),
334 source_account: "fee_source_account".to_string(),
335 fee: Some(200),
336 sequence_number: Some(54321),
337 transaction_input: crate::models::TransactionInput::SignedXdr {
338 xdr: "dummy_xdr".to_string(),
339 max_fee: 1_000_000,
340 },
341 network_passphrase: "Test SDF Network ; September 2015".to_string(),
342 memo: None,
343 valid_until: None,
344 signatures: Vec::new(),
345 simulation_transaction_data: None,
346 signed_envelope_xdr: None,
347 }),
348 valid_until: None,
349 network_type: NetworkType::Stellar,
350 noop_count: None,
351 is_canceled: Some(false),
352 delete_at: None,
353 };
354
355 let response = TransactionResponse::from(model.clone());
356
357 match response {
358 TransactionResponse::Stellar(stellar) => {
359 assert_eq!(stellar.id, model.id);
360 assert_eq!(stellar.hash, Some("fee_bump_hash_456".to_string()));
361 assert_eq!(stellar.status, TransactionStatus::Confirmed);
362 assert_eq!(stellar.created_at, now);
363 assert_eq!(stellar.sent_at, Some(now.clone()));
364 assert_eq!(stellar.confirmed_at, Some(now.clone()));
365 assert_eq!(stellar.source_account, "fee_source_account");
366 assert_eq!(stellar.fee, 200);
367 assert_eq!(stellar.sequence_number, 54321);
368 assert_eq!(stellar.relayer_id, "relayer3");
369 }
370 _ => panic!("Expected StellarTransactionResponse"),
371 }
372 }
373
374 #[test]
375 fn test_solana_default_recent_blockhash() {
376 let now = Utc::now().to_rfc3339();
377 let model = TransactionRepoModel {
378 id: "tx456".to_string(),
379 status: TransactionStatus::Pending,
380 status_reason: None,
381 created_at: now.clone(),
382 sent_at: None,
383 confirmed_at: None,
384 relayer_id: "relayer2".to_string(),
385 priced_at: None,
386 hashes: vec![],
387 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
388 transaction: "transaction_123".to_string(),
389 signature: None,
390 }),
391 valid_until: None,
392 network_type: NetworkType::Solana,
393 noop_count: None,
394 is_canceled: Some(false),
395 delete_at: None,
396 };
397
398 let response = TransactionResponse::from(model);
399
400 match response {
401 TransactionResponse::Solana(solana) => {
402 assert_eq!(solana.transaction, "transaction_123");
403 assert_eq!(solana.signature, None);
404 }
405 _ => panic!("Expected SolanaTransactionResponse"),
406 }
407 }
408}