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