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