1#[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#[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().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#[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 #[cfg_attr(feature = "serde", serde(flatten))]
58 prefix: MinerTransactionPrefix,
59 signatures: [(); 0],
60 },
61 V2 {
62 #[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 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#[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#[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#[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#[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}