cuprate_types/json/
block.rs

1//! JSON block types.
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use monero_serai::{block, transaction};
7
8use cuprate_helper::cast::usize_to_u64;
9
10use crate::{
11    hex::HexBytes,
12    json::output::{Output, TaggedKey, Target},
13};
14
15/// JSON representation of a block.
16///
17/// Used in:
18/// - [`/get_block` -> `json`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_block)
19#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub struct Block {
22    pub major_version: u8,
23    pub minor_version: u8,
24    pub timestamp: u64,
25    pub prev_id: HexBytes<32>,
26    pub nonce: u32,
27    pub miner_tx: MinerTransaction,
28    pub tx_hashes: Vec<HexBytes<32>>,
29}
30
31impl From<block::Block> for Block {
32    fn from(b: block::Block) -> Self {
33        let Ok(miner_tx) = MinerTransaction::try_from(b.miner_transaction) else {
34            unreachable!("input is a miner tx, this should never fail");
35        };
36
37        let tx_hashes = b.transactions.into_iter().map(HexBytes::<32>).collect();
38
39        Self {
40            major_version: b.header.hardfork_version,
41            minor_version: b.header.hardfork_signal,
42            timestamp: b.header.timestamp,
43            prev_id: HexBytes::<32>(b.header.previous),
44            nonce: b.header.nonce,
45            miner_tx,
46            tx_hashes,
47        }
48    }
49}
50
51/// [`Block::miner_tx`].
52#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54#[cfg_attr(feature = "serde", serde(untagged))]
55pub enum MinerTransaction {
56    V1 {
57        /// This field is [flattened](https://serde.rs/field-attrs.html#flatten).
58        #[cfg_attr(feature = "serde", serde(flatten))]
59        prefix: MinerTransactionPrefix,
60        signatures: [(); 0],
61    },
62    V2 {
63        /// This field is [flattened](https://serde.rs/field-attrs.html#flatten).
64        #[cfg_attr(feature = "serde", serde(flatten))]
65        prefix: MinerTransactionPrefix,
66        rct_signatures: MinerTransactionRctSignatures,
67    },
68}
69
70impl TryFrom<transaction::Transaction> for MinerTransaction {
71    type Error = transaction::Transaction;
72
73    /// # Errors
74    /// This function errors if the input is not a miner transaction.
75    fn try_from(tx: transaction::Transaction) -> Result<Self, transaction::Transaction> {
76        fn map_prefix(
77            prefix: transaction::TransactionPrefix,
78            version: u8,
79        ) -> Result<MinerTransactionPrefix, transaction::TransactionPrefix> {
80            let Some(input) = prefix.inputs.first() else {
81                return Err(prefix);
82            };
83
84            let height = match input {
85                transaction::Input::Gen(height) => usize_to_u64(*height),
86                transaction::Input::ToKey { .. } => return Err(prefix),
87            };
88
89            let vin = {
90                let r#gen = Gen { height };
91                let input = Input { r#gen };
92                [input]
93            };
94
95            let vout = prefix
96                .outputs
97                .into_iter()
98                .map(|o| {
99                    let amount = o.amount.unwrap_or(0);
100
101                    let target = match o.view_tag {
102                        Some(view_tag) => {
103                            let tagged_key = TaggedKey {
104                                key: HexBytes::<32>(o.key.0),
105                                view_tag: HexBytes::<1>([view_tag]),
106                            };
107
108                            Target::TaggedKey { tagged_key }
109                        }
110                        None => Target::Key {
111                            key: HexBytes::<32>(o.key.0),
112                        },
113                    };
114
115                    Output { amount, target }
116                })
117                .collect();
118
119            let unlock_time = match prefix.additional_timelock {
120                transaction::Timelock::None => 0,
121                transaction::Timelock::Block(x) => usize_to_u64(x),
122                transaction::Timelock::Time(x) => x,
123            };
124
125            Ok(MinerTransactionPrefix {
126                version,
127                unlock_time,
128                vin,
129                vout,
130                extra: prefix.extra,
131            })
132        }
133
134        Ok(match tx {
135            transaction::Transaction::V1 { prefix, signatures } => {
136                let prefix = match map_prefix(prefix, 1) {
137                    Ok(p) => p,
138                    Err(prefix) => return Err(transaction::Transaction::V1 { prefix, signatures }),
139                };
140
141                Self::V1 {
142                    prefix,
143                    signatures: [(); 0],
144                }
145            }
146            transaction::Transaction::V2 { prefix, proofs } => {
147                let prefix = match map_prefix(prefix, 2) {
148                    Ok(p) => p,
149                    Err(prefix) => return Err(transaction::Transaction::V2 { prefix, proofs }),
150                };
151
152                Self::V2 {
153                    prefix,
154                    rct_signatures: MinerTransactionRctSignatures { r#type: 0 },
155                }
156            }
157        })
158    }
159}
160
161impl Default for MinerTransaction {
162    fn default() -> Self {
163        Self::V1 {
164            prefix: Default::default(),
165            signatures: Default::default(),
166        }
167    }
168}
169
170/// [`MinerTransaction::V1::prefix`] & [`MinerTransaction::V2::prefix`].
171#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
172#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
173pub struct MinerTransactionPrefix {
174    pub version: u8,
175    pub unlock_time: u64,
176    pub vin: [Input; 1],
177    pub vout: Vec<Output>,
178    pub extra: Vec<u8>,
179}
180
181/// [`MinerTransaction::V2::rct_signatures`].
182#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
183#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
184pub struct MinerTransactionRctSignatures {
185    pub r#type: u8,
186}
187
188/// [`MinerTransactionPrefix::vin`].
189#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
190#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
191pub struct Input {
192    pub r#gen: Gen,
193}
194
195/// [`Input::gen`].
196#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
198pub struct Gen {
199    pub height: u64,
200}
201
202#[cfg(test)]
203mod test {
204    use hex_literal::hex;
205    use pretty_assertions::assert_eq;
206
207    use super::*;
208
209    #[expect(clippy::needless_pass_by_value)]
210    fn test(block: Block, block_json: &'static str) {
211        let json = serde_json::from_str::<Block>(block_json).unwrap();
212        assert_eq!(block, json);
213        let string = serde_json::to_string(&json).unwrap();
214        assert_eq!(block_json, &string);
215    }
216
217    #[test]
218    fn block_300000() {
219        const JSON: &str = r#"{"major_version":1,"minor_version":0,"timestamp":1415690591,"prev_id":"e97a0ab6307de9b9f9a9872263ef3e957976fb227eb9422c6854e989e5d5d34c","nonce":2147484616,"miner_tx":{"version":1,"unlock_time":300060,"vin":[{"gen":{"height":300000}}],"vout":[{"amount":47019296802,"target":{"key":"3c1dcbf5b485987ecef4596bb700e32cbc7bd05964e3888ffc05f8a46bf5fc33"}},{"amount":200000000000,"target":{"key":"5810afc7a1b01a1c913eb6aab15d4a851cbc4a8cf0adf90bb80ac1a7ca9928aa"}},{"amount":3000000000000,"target":{"key":"520f49c5f2ce8456dc1a565f35ed3a5ccfff3a1210b340870a57d2749a81a2df"}},{"amount":10000000000000,"target":{"key":"44d7705e62c76c2e349a474df6724aa1d9932092002b03a94f9c19d9d12b9427"}}],"extra":[1,251,8,189,254,12,213,173,108,61,156,198,144,151,31,130,141,211,120,55,81,98,32,247,111,127,254,170,170,240,124,190,223,2,8,0,0,0,64,184,115,46,246],"signatures":[]},"tx_hashes":[]}"#;
220
221        let block = Block {
222            major_version: 1,
223            minor_version: 0,
224            timestamp: 1415690591,
225            prev_id: HexBytes::<32>(hex!(
226                "e97a0ab6307de9b9f9a9872263ef3e957976fb227eb9422c6854e989e5d5d34c"
227            )),
228            nonce: 2147484616,
229            miner_tx: MinerTransaction::V1 {
230                prefix: MinerTransactionPrefix {
231                    version: 1,
232                    unlock_time: 300060,
233                    vin: [Input {
234                        r#gen: Gen { height: 300000 },
235                    }],
236                    vout: vec![
237                      Output {
238                        amount: 47019296802,
239                        target: Target::Key {
240                          key: HexBytes::<32>(hex!("3c1dcbf5b485987ecef4596bb700e32cbc7bd05964e3888ffc05f8a46bf5fc33")),
241                        }
242                      },
243                      Output {
244                        amount: 200000000000,
245                        target: Target::Key {
246                          key: HexBytes::<32>(hex!("5810afc7a1b01a1c913eb6aab15d4a851cbc4a8cf0adf90bb80ac1a7ca9928aa")),
247                        }
248                      },
249                      Output {
250                        amount: 3000000000000,
251                        target: Target::Key {
252                          key: HexBytes::<32>(hex!("520f49c5f2ce8456dc1a565f35ed3a5ccfff3a1210b340870a57d2749a81a2df")),
253                        }
254                      },
255                      Output {
256                        amount: 10000000000000,
257                        target: Target::Key {
258                          key: HexBytes::<32>(hex!("44d7705e62c76c2e349a474df6724aa1d9932092002b03a94f9c19d9d12b9427")),
259                        }
260                      }
261                    ],
262                    extra: vec![
263                        1, 251, 8, 189, 254, 12, 213, 173, 108, 61, 156, 198, 144, 151, 31, 130,
264                        141, 211, 120, 55, 81, 98, 32, 247, 111, 127, 254, 170, 170, 240, 124, 190,
265                        223, 2, 8, 0, 0, 0, 64, 184, 115, 46, 246,
266                    ],
267                },
268                signatures: [],
269            },
270            tx_hashes: vec![],
271        };
272
273        test(block, JSON);
274    }
275
276    #[test]
277    fn block_3245409() {
278        const JSON: &str = r#"{"major_version":16,"minor_version":16,"timestamp":1727293028,"prev_id":"41b56c273d69def3294e56179de71c61808042d54c1e085078d21dbe99e81b6f","nonce":311,"miner_tx":{"version":2,"unlock_time":3245469,"vin":[{"gen":{"height":3245409}}],"vout":[{"amount":601012280000,"target":{"tagged_key":{"key":"8c0b16c6df02b9944b49f375d96a958a0fc5431c048879bb5bf25f64a1163b9e","view_tag":"88"}}}],"extra":[1,39,23,182,203,58,48,15,217,9,13,147,104,133,206,176,185,56,237,179,136,72,84,129,113,98,206,4,18,50,130,162,94,2,17,73,18,21,33,32,112,5,0,0,0,0,0,0,0,0,0,0],"rct_signatures":{"type":0}},"tx_hashes":["eab76986a0cbcae690d8499f0f616f783fd2c89c6f611417f18011950dbdab2e","57b19aa8c2cdbb6836cf13dd1e321a67860965c12e4418f3c30f58c8899a851e","5340185432ab6b74fb21379f7e8d8f0e37f0882b2a7121fd7c08736f079e2edc","01dc6d31db56d68116f5294c1b4f80b33b048b5cdfefcd904f23e6c0de3daff5","c9fb6a2730678203948fef2a49fa155b63f35a3649f3d32ed405a6806f3bbd56","af965cdd2a2315baf1d4a3d242f44fe07b1fd606d5f4853c9ff546ca6c12a5af","97bc9e047d25fae8c14ce6ec882224e7b722f5e79b62a2602a6bacebdac8547b","28c46992eaf10dc0cceb313c30572d023432b7bd26e85e679bc8fe419533a7bf","c32e3acde2ff2885c9cc87253b40d6827d167dfcc3022c72f27084fd98788062","19e66a47f075c7cccde8a7b52803119e089e33e3a4847cace0bd1d17b0d22bab","8e8ac560e77a1ee72e82a5eb6887adbe5979a10cd29cb2c2a3720ce87db43a70","b7ff5141524b5cca24de6780a5dbfdf71e7de1e062fd85f557fb3b43b8e285dc","f09df0f113763ef9b9a2752ac293b478102f7cab03ef803a3d9db7585aea8912"]}"#;
279
280        let block = Block {
281            major_version: 16,
282            minor_version: 16,
283            timestamp: 1727293028,
284            prev_id: HexBytes::<32>(hex!(
285                "41b56c273d69def3294e56179de71c61808042d54c1e085078d21dbe99e81b6f"
286            )),
287            nonce: 311,
288            miner_tx: MinerTransaction::V2 {
289                prefix: MinerTransactionPrefix {
290                    version: 2,
291                    unlock_time: 3245469,
292                    vin: [Input {
293                        r#gen: Gen { height: 3245409 },
294                    }],
295                    vout: vec![Output {
296                        amount: 601012280000,
297                        target: Target::TaggedKey {
298                            tagged_key: TaggedKey {
299                                key: HexBytes::<32>(hex!(
300                                "8c0b16c6df02b9944b49f375d96a958a0fc5431c048879bb5bf25f64a1163b9e"
301                            )),
302                                view_tag: HexBytes::<1>(hex!("88")),
303                            },
304                        },
305                    }],
306                    extra: vec![
307                        1, 39, 23, 182, 203, 58, 48, 15, 217, 9, 13, 147, 104, 133, 206, 176, 185,
308                        56, 237, 179, 136, 72, 84, 129, 113, 98, 206, 4, 18, 50, 130, 162, 94, 2,
309                        17, 73, 18, 21, 33, 32, 112, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
310                    ],
311                },
312                rct_signatures: MinerTransactionRctSignatures { r#type: 0 },
313            },
314            tx_hashes: vec![
315                HexBytes::<32>(hex!(
316                    "eab76986a0cbcae690d8499f0f616f783fd2c89c6f611417f18011950dbdab2e"
317                )),
318                HexBytes::<32>(hex!(
319                    "57b19aa8c2cdbb6836cf13dd1e321a67860965c12e4418f3c30f58c8899a851e"
320                )),
321                HexBytes::<32>(hex!(
322                    "5340185432ab6b74fb21379f7e8d8f0e37f0882b2a7121fd7c08736f079e2edc"
323                )),
324                HexBytes::<32>(hex!(
325                    "01dc6d31db56d68116f5294c1b4f80b33b048b5cdfefcd904f23e6c0de3daff5"
326                )),
327                HexBytes::<32>(hex!(
328                    "c9fb6a2730678203948fef2a49fa155b63f35a3649f3d32ed405a6806f3bbd56"
329                )),
330                HexBytes::<32>(hex!(
331                    "af965cdd2a2315baf1d4a3d242f44fe07b1fd606d5f4853c9ff546ca6c12a5af"
332                )),
333                HexBytes::<32>(hex!(
334                    "97bc9e047d25fae8c14ce6ec882224e7b722f5e79b62a2602a6bacebdac8547b"
335                )),
336                HexBytes::<32>(hex!(
337                    "28c46992eaf10dc0cceb313c30572d023432b7bd26e85e679bc8fe419533a7bf"
338                )),
339                HexBytes::<32>(hex!(
340                    "c32e3acde2ff2885c9cc87253b40d6827d167dfcc3022c72f27084fd98788062"
341                )),
342                HexBytes::<32>(hex!(
343                    "19e66a47f075c7cccde8a7b52803119e089e33e3a4847cace0bd1d17b0d22bab"
344                )),
345                HexBytes::<32>(hex!(
346                    "8e8ac560e77a1ee72e82a5eb6887adbe5979a10cd29cb2c2a3720ce87db43a70"
347                )),
348                HexBytes::<32>(hex!(
349                    "b7ff5141524b5cca24de6780a5dbfdf71e7de1e062fd85f557fb3b43b8e285dc"
350                )),
351                HexBytes::<32>(hex!(
352                    "f09df0f113763ef9b9a2752ac293b478102f7cab03ef803a3d9db7585aea8912"
353                )),
354            ],
355        };
356
357        test(block, JSON);
358    }
359}