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