openzeppelin_relayer/models/transaction/
response.rs

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