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