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 metadata: None,
245 };
246
247 let response = TransactionResponse::from(model.clone());
248
249 match response {
250 TransactionResponse::Evm(evm) => {
251 assert_eq!(evm.id, model.id);
252 assert_eq!(evm.hash, Some("0xabc123".to_string()));
253 assert_eq!(evm.status, TransactionStatus::Pending);
254 assert_eq!(evm.created_at, now);
255 assert_eq!(evm.sent_at, Some(now.clone()));
256 assert_eq!(evm.confirmed_at, None);
257 assert_eq!(evm.gas_price, Some(20_000_000_000));
258 assert_eq!(evm.gas_limit, Some(21000));
259 assert_eq!(evm.nonce, Some(5));
260 assert_eq!(evm.value, U256::from(1000000000000000000u128));
261 assert_eq!(evm.from, "0xsender");
262 assert_eq!(evm.to, Some("0xrecipient".to_string()));
263 assert_eq!(evm.relayer_id, "relayer1");
264 }
265 _ => panic!("Expected EvmTransactionResponse"),
266 }
267 }
268
269 #[test]
270 fn test_from_transaction_repo_model_solana() {
271 let now = Utc::now().to_rfc3339();
272 let model = TransactionRepoModel {
273 id: "tx456".to_string(),
274 status: TransactionStatus::Confirmed,
275 status_reason: None,
276 created_at: now.clone(),
277 sent_at: Some(now.clone()),
278 confirmed_at: Some(now.clone()),
279 relayer_id: "relayer2".to_string(),
280 priced_at: None,
281 hashes: vec![],
282 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
283 transaction: Some("transaction_123".to_string()),
284 instructions: None,
285 signature: Some("signature_123".to_string()),
286 }),
287 valid_until: None,
288 network_type: NetworkType::Solana,
289 noop_count: None,
290 is_canceled: Some(false),
291 delete_at: None,
292 metadata: None,
293 };
294
295 let response = TransactionResponse::from(model.clone());
296
297 match response {
298 TransactionResponse::Solana(solana) => {
299 assert_eq!(solana.id, model.id);
300 assert_eq!(solana.status, TransactionStatus::Confirmed);
301 assert_eq!(solana.created_at, now);
302 assert_eq!(solana.sent_at, Some(now.clone()));
303 assert_eq!(solana.confirmed_at, Some(now.clone()));
304 assert_eq!(solana.transaction, "transaction_123");
305 assert_eq!(solana.signature, Some("signature_123".to_string()));
306 }
307 _ => panic!("Expected SolanaTransactionResponse"),
308 }
309 }
310
311 #[test]
312 fn test_from_transaction_repo_model_stellar() {
313 let now = Utc::now().to_rfc3339();
314 let model = TransactionRepoModel {
315 id: "tx789".to_string(),
316 status: TransactionStatus::Failed,
317 status_reason: None,
318 created_at: now.clone(),
319 sent_at: Some(now.clone()),
320 confirmed_at: Some(now.clone()),
321 relayer_id: "relayer3".to_string(),
322 priced_at: None,
323 hashes: vec![],
324 network_data: NetworkTransactionData::Stellar(StellarTransactionData {
325 hash: Some("stellar_hash_123".to_string()),
326 source_account: "source_account_id".to_string(),
327 fee: Some(100),
328 sequence_number: Some(12345),
329 transaction_input: crate::models::TransactionInput::Operations(vec![]),
330 network_passphrase: "Test SDF Network ; September 2015".to_string(),
331 memo: None,
332 valid_until: None,
333 signatures: Vec::new(),
334 simulation_transaction_data: None,
335 signed_envelope_xdr: None,
336 transaction_result_xdr: None,
337 }),
338 valid_until: None,
339 network_type: NetworkType::Stellar,
340 noop_count: None,
341 is_canceled: Some(false),
342 delete_at: None,
343 metadata: None,
344 };
345
346 let response = TransactionResponse::from(model.clone());
347
348 match response {
349 TransactionResponse::Stellar(stellar) => {
350 assert_eq!(stellar.id, model.id);
351 assert_eq!(stellar.hash, Some("stellar_hash_123".to_string()));
352 assert_eq!(stellar.status, TransactionStatus::Failed);
353 assert_eq!(stellar.created_at, now);
354 assert_eq!(stellar.sent_at, Some(now.clone()));
355 assert_eq!(stellar.confirmed_at, Some(now.clone()));
356 assert_eq!(stellar.source_account, "source_account_id");
357 assert_eq!(stellar.fee, 100);
358 assert_eq!(stellar.sequence_number, 12345);
359 assert_eq!(stellar.relayer_id, "relayer3");
360 }
361 _ => panic!("Expected StellarTransactionResponse"),
362 }
363 }
364
365 #[test]
366 fn test_stellar_fee_bump_transaction_response() {
367 let now = Utc::now().to_rfc3339();
368 let model = TransactionRepoModel {
369 id: "tx-fee-bump".to_string(),
370 status: TransactionStatus::Confirmed,
371 status_reason: None,
372 created_at: now.clone(),
373 sent_at: Some(now.clone()),
374 confirmed_at: Some(now.clone()),
375 relayer_id: "relayer3".to_string(),
376 priced_at: None,
377 hashes: vec!["fee_bump_hash_456".to_string()],
378 network_data: NetworkTransactionData::Stellar(StellarTransactionData {
379 hash: Some("fee_bump_hash_456".to_string()),
380 source_account: "fee_source_account".to_string(),
381 fee: Some(200),
382 sequence_number: Some(54321),
383 transaction_input: crate::models::TransactionInput::SignedXdr {
384 xdr: "dummy_xdr".to_string(),
385 max_fee: 1_000_000,
386 },
387 network_passphrase: "Test SDF Network ; September 2015".to_string(),
388 memo: None,
389 valid_until: None,
390 signatures: Vec::new(),
391 simulation_transaction_data: None,
392 signed_envelope_xdr: None,
393 transaction_result_xdr: None,
394 }),
395 valid_until: None,
396 network_type: NetworkType::Stellar,
397 noop_count: None,
398 is_canceled: Some(false),
399 delete_at: None,
400 metadata: None,
401 };
402
403 let response = TransactionResponse::from(model.clone());
404
405 match response {
406 TransactionResponse::Stellar(stellar) => {
407 assert_eq!(stellar.id, model.id);
408 assert_eq!(stellar.hash, Some("fee_bump_hash_456".to_string()));
409 assert_eq!(stellar.status, TransactionStatus::Confirmed);
410 assert_eq!(stellar.created_at, now);
411 assert_eq!(stellar.sent_at, Some(now.clone()));
412 assert_eq!(stellar.confirmed_at, Some(now.clone()));
413 assert_eq!(stellar.source_account, "fee_source_account");
414 assert_eq!(stellar.fee, 200);
415 assert_eq!(stellar.sequence_number, 54321);
416 assert_eq!(stellar.relayer_id, "relayer3");
417 }
418 _ => panic!("Expected StellarTransactionResponse"),
419 }
420 }
421
422 #[test]
423 fn test_solana_default_recent_blockhash() {
424 let now = Utc::now().to_rfc3339();
425 let model = TransactionRepoModel {
426 id: "tx456".to_string(),
427 status: TransactionStatus::Pending,
428 status_reason: None,
429 created_at: now.clone(),
430 sent_at: None,
431 confirmed_at: None,
432 relayer_id: "relayer2".to_string(),
433 priced_at: None,
434 hashes: vec![],
435 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
436 transaction: Some("transaction_123".to_string()),
437 instructions: None,
438 signature: None,
439 }),
440 valid_until: None,
441 network_type: NetworkType::Solana,
442 noop_count: None,
443 is_canceled: Some(false),
444 delete_at: None,
445 metadata: None,
446 };
447
448 let response = TransactionResponse::from(model);
449
450 match response {
451 TransactionResponse::Solana(solana) => {
452 assert_eq!(solana.transaction, "transaction_123");
453 assert_eq!(solana.signature, None);
454 }
455 _ => panic!("Expected SolanaTransactionResponse"),
456 }
457 }
458}