Skip to main content

cuprate_types/json/
block.rs

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