openzeppelin_relayer/models/transaction/
response.rs

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), // 1 ETH
153                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, // Testing the default case
352                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, ""); // Should be default empty string
366            }
367            _ => panic!("Expected SolanaTransactionResponse"),
368        }
369    }
370}