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