onemoney_protocol/types/responses/
checkpoints.rs

1//! Checkpoint-related API response types.
2
3use crate::Transaction;
4use crate::types::responses::transactions::Hash;
5use serde::{Deserialize, Serialize};
6use std::fmt::{Display, Formatter, Result as FmtResult};
7
8/// Checkpoint transactions representation.
9/// This can be either transaction hashes or full transaction objects.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum CheckpointTransactions {
13    /// Full transaction objects
14    Full(Vec<Transaction>),
15    /// Only transaction hashes
16    Hashes(Vec<Hash>),
17}
18
19impl Display for CheckpointTransactions {
20    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
21        match self {
22            CheckpointTransactions::Full(transactions) => {
23                write!(
24                    f,
25                    "Checkpoint with {} full transactions",
26                    transactions.len()
27                )
28            }
29            CheckpointTransactions::Hashes(hashes) => {
30                write!(f, "Checkpoint with {} transaction hashes", hashes.len())
31            }
32        }
33    }
34}
35
36/// A checkpoint includes header data and transactions.
37/// Header fields are flattened at the top level to match L1 server format.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Checkpoint {
40    /// Hash of the checkpoint.
41    pub hash: Hash,
42    /// Hash of the parent.
43    pub parent_hash: Hash,
44    /// State root hash.
45    pub state_root: Hash,
46    /// Transactions root hash.
47    pub transactions_root: Hash,
48    /// Transactions receipts root hash.
49    pub receipts_root: Hash,
50    /// Checkpoint number.
51    pub number: u64,
52    /// Timestamp.
53    pub timestamp: u64,
54    /// Extra data.
55    pub extra_data: String,
56    /// Checkpoint transactions (either hashes or full objects).
57    pub transactions: CheckpointTransactions,
58    /// Integer the size of this checkpoint in bytes.
59    pub size: Option<u64>,
60}
61
62impl Display for Checkpoint {
63    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
64        writeln!(f, "Checkpoint #{}:", self.number)?;
65        writeln!(f, "  Hash: {}", self.hash.hash)?;
66        writeln!(f, "  Parent Hash: {}", self.parent_hash.hash)?;
67        writeln!(f, "  State Root: {}", self.state_root.hash)?;
68        writeln!(f, "  Transactions Root: {}", self.transactions_root.hash)?;
69        writeln!(f, "  Receipts Root: {}", self.receipts_root.hash)?;
70        writeln!(f, "  Timestamp: {}", self.timestamp)?;
71        writeln!(f, "  Extra Data: {}", self.extra_data)?;
72
73        if let Some(size) = self.size {
74            writeln!(f, "  Size: {} bytes", size)?;
75        }
76
77        writeln!(f, "  Transactions:")?;
78        match &self.transactions {
79            CheckpointTransactions::Full(transactions) => {
80                writeln!(
81                    f,
82                    "    Count: {} (full transaction details)",
83                    transactions.len()
84                )?;
85                for (i, tx) in transactions.iter().enumerate() {
86                    writeln!(f, "    Transaction {}:", i + 1)?;
87                    writeln!(f, "      Hash: {}", tx.hash)?;
88                    writeln!(f, "      From: {}", tx.from)?;
89                    writeln!(f, "      Nonce: {}", tx.nonce)?;
90                    writeln!(f, "      Epoch: {}", tx.epoch)?;
91                    writeln!(f, "      Checkpoint: {}", tx.checkpoint)?;
92                    writeln!(f, "      Chain ID: {}", tx.chain_id)?;
93
94                    if let Some(checkpoint_hash) = &tx.checkpoint_hash {
95                        writeln!(f, "      Checkpoint Hash: {}", checkpoint_hash)?;
96                    }
97                    if let Some(checkpoint_number) = tx.checkpoint_number {
98                        writeln!(f, "      Checkpoint Number: {}", checkpoint_number)?;
99                    }
100                    if let Some(transaction_index) = tx.transaction_index {
101                        writeln!(f, "      Transaction Index: {}", transaction_index)?;
102                    }
103
104                    writeln!(f, "      Signature:")?;
105                    writeln!(f, "        R: {}", tx.signature.r)?;
106                    writeln!(f, "        S: {}", tx.signature.s)?;
107                    writeln!(f, "        V: {}", tx.signature.v)?;
108
109                    writeln!(f, "      Payload: {:?}", tx.data)?;
110
111                    if i < transactions.len() - 1 {
112                        writeln!(f)?;
113                    }
114                }
115            }
116            CheckpointTransactions::Hashes(hashes) => {
117                writeln!(f, "    Count: {} (hashes only)", hashes.len())?;
118                for (i, hash) in hashes.iter().enumerate() {
119                    writeln!(f, "    {}: {}", i + 1, hash.hash)?;
120                }
121            }
122        }
123
124        Ok(())
125    }
126}
127
128/// Checkpoint header representation.
129/// This is kept for backward compatibility but consider using flattened Checkpoint.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct CheckpointHeader {
132    /// Hash of the checkpoint.
133    pub hash: Hash,
134    /// Hash of the parent.
135    pub parent_hash: Hash,
136    /// State root hash.
137    pub state_root: Hash,
138    /// Transactions root hash.
139    pub transactions_root: Hash,
140    /// Transactions receipts root hash.
141    pub receipts_root: Hash,
142    /// Checkpoint number.
143    pub number: u64,
144    /// Timestamp.
145    pub timestamp: u64,
146    /// Extra data.
147    pub extra_data: String,
148}
149
150impl Display for CheckpointHeader {
151    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
152        write!(
153            f,
154            "Checkpoint #{}: {} (parent: {}, timestamp: {})",
155            self.number, self.hash, self.parent_hash, self.timestamp
156        )
157    }
158}
159
160/// Checkpoint number response.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct CheckpointNumber {
163    /// Current checkpoint number.
164    pub number: u64,
165}
166
167impl Display for CheckpointNumber {
168    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
169        write!(f, "Checkpoint Number: {}", self.number)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::types::responses::transactions::*;
177    use alloy_primitives::B256;
178    use std::str::FromStr;
179
180    /// Helper function to create Hash from hex string
181    fn create_hash(hex_str: &str) -> Hash {
182        // Pad short hex strings to 32 bytes (64 hex chars + 0x prefix)
183        let padded_hex = if hex_str.len() < 66 {
184            let without_prefix = hex_str.strip_prefix("0x").unwrap_or(hex_str);
185            format!("0x{:0<64}", without_prefix)
186        } else {
187            hex_str.to_string()
188        };
189
190        Hash {
191            hash: B256::from_str(&padded_hex).expect("Test data should be valid"),
192        }
193    }
194
195    #[test]
196    fn test_checkpoint_number_structure() {
197        let checkpoint_num = CheckpointNumber { number: 12345 };
198
199        // Test serialization
200        let json = serde_json::to_string(&checkpoint_num).expect("Test data should be valid");
201        let deserialized: CheckpointNumber =
202            serde_json::from_str(&json).expect("Test data should be valid");
203
204        assert_eq!(checkpoint_num.number, deserialized.number);
205
206        // Test display
207        let display_str = format!("{}", checkpoint_num);
208        assert_eq!(display_str, "Checkpoint Number: 12345");
209
210        // Test clone
211        let cloned_num = checkpoint_num.clone();
212        assert_eq!(checkpoint_num.number, cloned_num.number);
213
214        // Test debug output
215        let debug_num_str = format!("{:?}", checkpoint_num);
216        assert!(debug_num_str.contains("CheckpointNumber"));
217        assert!(debug_num_str.contains("12345"));
218    }
219
220    #[test]
221    fn test_checkpoint_transactions_hashes() {
222        let hashes = vec![
223            create_hash("0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777"),
224            create_hash("0x20e081da293ae3b81e30f864f38f6911663d7f2cf98337fca38db3cf5bbe7a8f"),
225            create_hash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"),
226        ];
227
228        // Test individual Hash serialization is transparent (not an object)
229        let single_hash_json =
230            serde_json::to_string(&hashes[0]).expect("Hash serialization should work");
231        assert!(single_hash_json.starts_with("\"0x"));
232        assert!(single_hash_json.ends_with("\""));
233        assert!(!single_hash_json.contains("{"));
234        assert!(!single_hash_json.contains("hash"));
235
236        let checkpoint_transactions = CheckpointTransactions::Hashes(hashes.clone());
237
238        // Test serialization
239        let json =
240            serde_json::to_string(&checkpoint_transactions).expect("Test data should be valid");
241        let deserialized: CheckpointTransactions =
242            serde_json::from_str(&json).expect("Test data should be valid");
243
244        match deserialized {
245            CheckpointTransactions::Hashes(deserialized_hashes) => {
246                assert_eq!(deserialized_hashes.len(), 3);
247                assert_eq!(deserialized_hashes, hashes);
248            }
249            _ => panic!("Should be Hashes variant"),
250        }
251
252        // Test display
253        let display_str = format!("{}", checkpoint_transactions);
254        assert!(display_str.contains("Checkpoint with 3 transaction hashes"));
255    }
256
257    #[test]
258    fn test_checkpoint_transactions_empty() {
259        // Test empty transactions
260        let empty_full = CheckpointTransactions::Full(vec![]);
261        let display_str = format!("{}", empty_full);
262        assert!(display_str.contains("Checkpoint with 0 full transactions"));
263
264        let empty_hashes = CheckpointTransactions::Hashes(vec![]);
265        let display_str = format!("{}", empty_hashes);
266        assert!(display_str.contains("Checkpoint with 0 transaction hashes"));
267    }
268
269    #[test]
270    fn test_checkpoint_header_structure() {
271        let header = CheckpointHeader {
272            hash: create_hash("0x123abc456def789012345678901234567890123456789012345678901234567e"),
273            parent_hash: create_hash(
274                "0x000abc456def789012345678901234567890123456789012345678901234567e",
275            ),
276            state_root: create_hash(
277                "0xabc123456def789012345678901234567890123456789012345678901234567e",
278            ),
279            transactions_root: create_hash(
280                "0xdef456789012345678901234567890123456789012345678901234567890abc1",
281            ),
282            receipts_root: create_hash(
283                "0x789012345678901234567890123456789012345678901234567890abc123def4",
284            ),
285            number: 999,
286            timestamp: 1703097600,
287            extra_data: "0x1234".to_string(),
288        };
289
290        // Test serialization
291        let json = serde_json::to_string(&header).expect("Test data should be valid");
292        let deserialized: CheckpointHeader =
293            serde_json::from_str(&json).expect("Test data should be valid");
294
295        assert_eq!(header.hash, deserialized.hash);
296        assert_eq!(header.parent_hash, deserialized.parent_hash);
297        assert_eq!(header.state_root, deserialized.state_root);
298        assert_eq!(header.transactions_root, deserialized.transactions_root);
299        assert_eq!(header.receipts_root, deserialized.receipts_root);
300        assert_eq!(header.number, deserialized.number);
301        assert_eq!(header.timestamp, deserialized.timestamp);
302        assert_eq!(header.extra_data, deserialized.extra_data);
303
304        // Test display
305        let display_str = format!("{}", header);
306        assert!(display_str.contains("Checkpoint #999:"));
307        assert!(display_str.contains(&header.hash.hash.to_string()));
308        assert!(display_str.contains(&header.parent_hash.hash.to_string()));
309        assert!(display_str.contains("1703097600"));
310    }
311
312    #[test]
313    fn test_checkpoint_with_various_sizes() {
314        let checkpoint_with_size = Checkpoint {
315            hash: create_hash("0x123abc"),
316            parent_hash: create_hash("0x000abc"),
317            state_root: create_hash("0xabc123"),
318            transactions_root: create_hash("0xdef456"),
319            receipts_root: create_hash("0x789012"),
320            number: 100,
321            timestamp: 1703097600,
322            extra_data: "0x".to_string(),
323            transactions: CheckpointTransactions::Hashes(vec![]),
324            size: Some(2048),
325        };
326
327        let checkpoint_without_size = Checkpoint {
328            hash: create_hash("0x123abc"),
329            parent_hash: create_hash("0x000abc"),
330            state_root: create_hash("0xabc123"),
331            transactions_root: create_hash("0xdef456"),
332            receipts_root: create_hash("0x789012"),
333            number: 100,
334            timestamp: 1703097600,
335            extra_data: "0x".to_string(),
336            transactions: CheckpointTransactions::Hashes(vec![]),
337            size: None,
338        };
339
340        // Test serialization with size
341        let json_with_size =
342            serde_json::to_string(&checkpoint_with_size).expect("Test data should be valid");
343        let deserialized_with_size: Checkpoint =
344            serde_json::from_str(&json_with_size).expect("Test data should be valid");
345        assert_eq!(checkpoint_with_size.size, deserialized_with_size.size);
346
347        // Test serialization without size
348        let json_without_size =
349            serde_json::to_string(&checkpoint_without_size).expect("Test data should be valid");
350        let deserialized_without_size: Checkpoint =
351            serde_json::from_str(&json_without_size).expect("Test data should be valid");
352        assert_eq!(checkpoint_without_size.size, deserialized_without_size.size);
353        assert!(deserialized_without_size.size.is_none());
354    }
355
356    #[test]
357    fn test_checkpoint_with_hashes_only() {
358        let checkpoint = Checkpoint {
359            hash: create_hash("0x123abc456def789012345678901234567890123456789012345678901234567e"),
360            parent_hash: create_hash(
361                "0x000abc456def789012345678901234567890123456789012345678901234567e",
362            ),
363            state_root: create_hash(
364                "0xabc123456def789012345678901234567890123456789012345678901234567e",
365            ),
366            transactions_root: create_hash(
367                "0xdef456789012345678901234567890123456789012345678901234567890abc1",
368            ),
369            receipts_root: create_hash(
370                "0x789012345678901234567890123456789012345678901234567890abc123def4",
371            ),
372            number: 1500,
373            timestamp: 1703097600,
374            extra_data: "checkpoint_data".to_string(),
375            transactions: CheckpointTransactions::Hashes(vec![
376                create_hash("0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777"),
377                create_hash("0x20e081da293ae3b81e30f864f38f6911663d7f2cf98337fca38db3cf5bbe7a8f"),
378            ]),
379            size: None, // Test without size
380        };
381
382        // Test display with hashes
383        let display_str = format!("{}", checkpoint);
384        assert!(display_str.contains("Checkpoint #1500:"));
385        assert!(display_str.contains("checkpoint_data"));
386        assert!(!display_str.contains("Size:")); // Should not contain size when None
387        assert!(display_str.contains("hashes only"));
388        assert!(
389            display_str
390                .contains("1: 0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777")
391        );
392        assert!(
393            display_str
394                .contains("2: 0x20e081da293ae3b81e30f864f38f6911663d7f2cf98337fca38db3cf5bbe7a8f")
395        );
396    }
397
398    #[test]
399    fn test_serde_untagged_enum() {
400        // Test that the untagged enum properly deserializes different JSON structures
401
402        // JSON with array of hashes (should deserialize as Hashes)
403        let hashes_json = r#"["0x902006665c369834a0cf52eea2780f934a90b3c86a3918fb57371ac1fbbd7777", "0x20e081da293ae3b81e30f864f38f6911663d7f2cf98337fca38db3cf5bbe7a8f"]"#;
404        let parsed: CheckpointTransactions =
405            serde_json::from_str(hashes_json).expect("Test data should be valid");
406
407        match parsed {
408            CheckpointTransactions::Hashes(hashes) => {
409                assert_eq!(hashes.len(), 2);
410            }
411            _ => panic!("Should parse as Hashes variant"),
412        }
413
414        // Note: Testing full transaction parsing would require a complete transaction JSON structure
415        // with all required fields. For now, we've tested the Hashes variant which works correctly.
416        // The untagged enum will automatically choose the correct variant based on the JSON structure.
417    }
418}