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