openzeppelin_relayer/models/transaction/
response.rs

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