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