1use crate::error::{Error, Result};
2use crate::model::{Address, Base58String, SignedTransaction};
3use crate::util::JsonDeserializer;
4use serde_json::Value;
5use std::borrow::Borrow;
6
7#[derive(Clone, Eq, PartialEq, Debug)]
8pub struct BlockHeaders {
9 version: u8,
10 timestamp: u64,
11 reference: Base58String,
12 nxt_consensus: NxtConsensus,
13 transactions_root: Base58String,
14 id: Base58String,
15 features: Vec<u32>,
16 desired_reward: i64,
17 generator: Address,
18 signature: Base58String,
19 blocksize: u32,
20 transaction_count: u32,
21 height: u32,
22 total_fee: u64,
23 reward: u64,
24 vrf: Base58String,
25}
26
27#[allow(clippy::too_many_arguments)]
28impl BlockHeaders {
29 pub fn new(
30 version: u8,
31 timestamp: u64,
32 reference: Base58String,
33 nxt_consensus: NxtConsensus,
34 transactions_root: Base58String,
35 id: Base58String,
36 features: Vec<u32>,
37 desired_reward: i64,
38 generator: Address,
39 signature: Base58String,
40 blocksize: u32,
41 transaction_count: u32,
42 height: u32,
43 total_fee: u64,
44 reward: u64,
45 vrf: Base58String,
46 ) -> Self {
47 Self {
48 version,
49 timestamp,
50 reference,
51 nxt_consensus,
52 transactions_root,
53 id,
54 features,
55 desired_reward,
56 generator,
57 signature,
58 blocksize,
59 transaction_count,
60 height,
61 total_fee,
62 reward,
63 vrf,
64 }
65 }
66
67 pub fn version(&self) -> u8 {
68 self.version
69 }
70
71 pub fn timestamp(&self) -> u64 {
72 self.timestamp
73 }
74
75 pub fn reference(&self) -> Base58String {
76 self.reference.clone()
77 }
78
79 pub fn nxt_consensus(&self) -> NxtConsensus {
80 self.nxt_consensus.clone()
81 }
82
83 pub fn transactions_root(&self) -> Base58String {
84 self.transactions_root.clone()
85 }
86
87 pub fn id(&self) -> Base58String {
88 self.id.clone()
89 }
90
91 pub fn features(&self) -> Vec<u32> {
92 self.features.clone()
93 }
94
95 pub fn desired_reward(&self) -> i64 {
96 self.desired_reward
97 }
98
99 pub fn generator(&self) -> Address {
100 self.generator.clone()
101 }
102
103 pub fn signature(&self) -> Base58String {
104 self.signature.clone()
105 }
106
107 pub fn blocksize(&self) -> u32 {
108 self.blocksize
109 }
110
111 pub fn transaction_count(&self) -> u32 {
112 self.transaction_count
113 }
114
115 pub fn height(&self) -> u32 {
116 self.height
117 }
118
119 pub fn total_fee(&self) -> u64 {
120 self.total_fee
121 }
122
123 pub fn reward(&self) -> u64 {
124 self.reward
125 }
126
127 pub fn vrf(&self) -> Base58String {
128 self.vrf.clone()
129 }
130}
131
132impl TryFrom<&Value> for BlockHeaders {
133 type Error = Error;
134
135 fn try_from(value: &Value) -> Result<Self> {
136 let version = JsonDeserializer::safe_to_int_from_field(value, "version")?;
137 let timestamp = JsonDeserializer::safe_to_int_from_field(value, "timestamp")?;
138 let reference = JsonDeserializer::safe_to_string_from_field(value, "reference")?;
139 let transactions_root = match value["transactionsRoot"].as_str() {
140 Some(val) => Base58String::from_string(val)?,
141 None => Base58String::empty(),
142 };
143 let id = JsonDeserializer::safe_to_string_from_field(value, "id")?;
144 let features = match value["features"].as_array() {
145 Some(array) => array
146 .iter()
147 .filter_map(|value| value.as_i64())
148 .map(|feature| feature as u32)
149 .collect(),
150 None => vec![],
151 };
152
153 let desired_reward = value["desiredReward"].as_i64().unwrap_or(0);
154 let generator = JsonDeserializer::safe_to_string_from_field(value, "generator")?;
155 let signature = JsonDeserializer::safe_to_string_from_field(value, "signature")?;
156 let blocksize = JsonDeserializer::safe_to_int_from_field(value, "blocksize")?;
157 let transaction_count =
158 JsonDeserializer::safe_to_int_from_field(value, "transactionCount")?;
159 let height = JsonDeserializer::safe_to_int_from_field(value, "height")?;
160 let total_fee = JsonDeserializer::safe_to_int_from_field(value, "totalFee")?;
161 let reward = value["reward"].as_i64().unwrap_or(0);
162
163 let vrf = value["VRF"].as_str().unwrap_or("");
164 Ok(BlockHeaders {
165 version: version as u8,
166 timestamp: timestamp as u64,
167 reference: Base58String::from_string(&reference)?,
168 nxt_consensus: value["nxt-consensus"].borrow().try_into()?,
169 transactions_root,
170 id: Base58String::from_string(&id)?,
171 features,
172 desired_reward,
173 generator: Address::from_string(&generator)?,
174 signature: Base58String::from_string(&signature)?,
175 blocksize: blocksize as u32,
176 transaction_count: transaction_count as u32,
177 height: height as u32,
178 total_fee: total_fee as u64,
179 reward: reward as u64,
180 vrf: Base58String::from_string(vrf)?,
181 })
182 }
183}
184
185#[derive(Clone, Eq, PartialEq, Debug)]
186pub struct NxtConsensus {
187 base_target: u32,
188 generation_signature: Base58String,
189}
190
191impl NxtConsensus {
192 pub fn new(base_target: u32, generation_signature: Base58String) -> Self {
193 Self {
194 base_target,
195 generation_signature,
196 }
197 }
198
199 pub fn base_target(&self) -> u32 {
200 self.base_target
201 }
202
203 pub fn generation_signature(&self) -> Base58String {
204 self.generation_signature.clone()
205 }
206}
207
208impl TryFrom<&Value> for NxtConsensus {
209 type Error = Error;
210
211 fn try_from(value: &Value) -> Result<Self> {
212 let base_target = JsonDeserializer::safe_to_int_from_field(value, "base-target")?;
213 let generation_signature =
214 JsonDeserializer::safe_to_string_from_field(value, "generation-signature")?;
215
216 Ok(NxtConsensus {
217 base_target: base_target as u32,
218 generation_signature: Base58String::from_string(&generation_signature)?,
219 })
220 }
221}
222
223#[derive(Clone, Eq, PartialEq, Debug)]
224pub struct Block {
225 block_headers: BlockHeaders,
226 fee: u64,
227 transactions: Vec<SignedTransaction>,
228}
229
230impl Block {
231 pub fn new(
232 block_headers: BlockHeaders,
233 fee: u64,
234 transactions: Vec<SignedTransaction>,
235 ) -> Self {
236 Self {
237 block_headers,
238 fee,
239 transactions,
240 }
241 }
242
243 pub fn block_headers(&self) -> BlockHeaders {
244 self.block_headers.clone()
245 }
246
247 pub fn fee(&self) -> u64 {
248 self.fee
249 }
250
251 pub fn transactions(&self) -> Vec<SignedTransaction> {
252 self.transactions.clone()
253 }
254}
255
256impl TryFrom<&Value> for Block {
257 type Error = Error;
258
259 fn try_from(value: &Value) -> Result<Self> {
260 let block_headers = value.try_into()?;
261 let fee = JsonDeserializer::safe_to_int_from_field(value, "fee")?;
262
263 let transactions: Vec<SignedTransaction> =
264 JsonDeserializer::safe_to_array_from_field(value, "transactions")?
265 .iter()
266 .map(|tx| tx.try_into())
267 .collect::<Result<Vec<SignedTransaction>>>()?;
268 Ok(Self {
269 block_headers,
270 fee: fee as u64,
271 transactions,
272 })
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use crate::error::Result;
279 use crate::model::{Block, BlockHeaders, ByteString};
280 use serde_json::Value;
281 use std::borrow::Borrow;
282 use std::fs;
283
284 #[test]
285 pub fn block_headers_from_response() -> Result<()> {
286 let data = fs::read_to_string("./tests/resources/blocks/block_headers_rs.json")
287 .expect("Unable to read file");
288 let json: Value = serde_json::from_str(&data).expect("failed to generate json from str");
289
290 let block_headers: BlockHeaders = json.borrow().try_into()?;
291
292 assert_eq!(5, block_headers.version());
293 assert_eq!(1662965069859, block_headers.timestamp());
294 assert_eq!(
295 "FDVU5z5JcjU1rU6BV7MassmFwzfiabxVUBcDxzJjZTNu",
296 block_headers.reference().encoded()
297 );
298 assert_eq!(288, block_headers.nxt_consensus().base_target());
299 assert_eq!(
300 "2DzzTwTrDvNnKa5BnkQyuGJ6rAmCTxrKwQNdAEiRPPiY1WkrCy5ge5FDAWsMPh3eAk3sRQq3iVB8ZVGFkP8uJU65AfR4rkQw687yNcA3wbsDsDjsoyir1nGFCWkA34jv5cUN",
301 block_headers.nxt_consensus().generation_signature().encoded());
302
303 assert_eq!(
304 "HW2HdCDGFX1cKQC9VQHPbCi5S87jzB43m8bM1yvbjxN6",
305 block_headers.transactions_root().encoded()
306 );
307 assert_eq!(
308 "7FjvruZHvR3mi9YvTaiGQ93uvdHbarcL54ynrS6jN1Vq",
309 block_headers.id().encoded()
310 );
311 assert_eq!(3, block_headers.features()[0]);
312 assert_eq!(-1, block_headers.desired_reward());
313 assert_eq!(
314 "3Mxv6Dpa1qRuyQBRFg3GwUaf3rcjHqWwNmC",
315 block_headers.generator().encoded()
316 );
317 assert_eq!("3BCrNxGjwoxRMX2vxshN2K5ucSzt8huwvkHK54qWnp6UVQLBKx1TaidWAoxtswWPxbLTdmkWAMrCEdUVUZw4o8Zr", block_headers.signature().encoded());
318 assert_eq!(869, block_headers.blocksize());
319 assert_eq!(1, block_headers.transaction_count());
320 assert_eq!(2225531, block_headers.height());
321 assert_eq!(500000, block_headers.total_fee());
322 assert_eq!(600000000, block_headers.reward());
323 assert_eq!(
324 "BaHPQwcWxXPaaUiRgavbEg5hkAvzWdNrLYV3x5JEDgpJ",
325 block_headers.vrf().encoded()
326 );
327 Ok(())
328 }
329
330 #[test]
331 pub fn block_at_height_response() -> Result<()> {
332 let data = fs::read_to_string("./tests/resources/blocks/block_rs.json")
333 .expect("Unable to read file");
334 let json: Value = serde_json::from_str(&data).expect("failed to generate json from str");
335
336 let block: Block = json.borrow().try_into()?;
337 let block_headers = block.block_headers();
338
339 assert_eq!(5, block_headers.version());
340 assert_eq!(1662965069859, block_headers.timestamp());
341 assert_eq!(
342 "FDVU5z5JcjU1rU6BV7MassmFwzfiabxVUBcDxzJjZTNu",
343 block_headers.reference().encoded()
344 );
345 assert_eq!(288, block_headers.nxt_consensus().base_target());
346 assert_eq!(
347 "2DzzTwTrDvNnKa5BnkQyuGJ6rAmCTxrKwQNdAEiRPPiY1WkrCy5ge5FDAWsMPh3eAk3sRQq3iVB8ZVGFkP8uJU65AfR4rkQw687yNcA3wbsDsDjsoyir1nGFCWkA34jv5cUN",
348 block_headers.nxt_consensus().generation_signature().encoded());
349
350 assert_eq!(
351 "HW2HdCDGFX1cKQC9VQHPbCi5S87jzB43m8bM1yvbjxN6",
352 block_headers.transactions_root().encoded()
353 );
354 assert_eq!(
355 "7FjvruZHvR3mi9YvTaiGQ93uvdHbarcL54ynrS6jN1Vq",
356 block_headers.id().encoded()
357 );
358 assert_eq!(3, block_headers.features()[0]);
359 assert_eq!(-1, block_headers.desired_reward());
360 assert_eq!(
361 "3Mxv6Dpa1qRuyQBRFg3GwUaf3rcjHqWwNmC",
362 block_headers.generator().encoded()
363 );
364 assert_eq!("3BCrNxGjwoxRMX2vxshN2K5ucSzt8huwvkHK54qWnp6UVQLBKx1TaidWAoxtswWPxbLTdmkWAMrCEdUVUZw4o8Zr", block_headers.signature().encoded());
365 assert_eq!(869, block_headers.blocksize());
366 assert_eq!(1, block_headers.transaction_count());
367 assert_eq!(2225531, block_headers.height());
368 assert_eq!(500000, block_headers.total_fee());
369 assert_eq!(600000000, block_headers.reward());
370 assert_eq!(
371 "BaHPQwcWxXPaaUiRgavbEg5hkAvzWdNrLYV3x5JEDgpJ",
372 block_headers.vrf().encoded()
373 );
374
375 assert_eq!(500000, block.fee());
376 assert_eq!(19, block.transactions().len());
377
378 let genesis = &block.transactions[0];
379 assert_eq!(
380 "3zpi4i5SeCoaiCBn1iuTUvCc5aahvtabqXBTrCXy1Y3ujUbJo56VVv6n4HQtcwiFapvg3BKV6stb5QkxsBrudTKZ",
381 genesis.id()?.encoded()
382 );
383 let payment = &block.transactions[1];
384 assert_eq!(
385 "3MBsS7S42PVEM8c1XxLsGsxzhitPsyaazDs1QoE26pCTHdRMYRv7n984wmjSFP863iZ2GR28aunSVvPC8sooEpbP",
386 payment.id()?.encoded()
387 );
388 let issue = &block.transactions[2];
389 assert_eq!(
390 "3kuZKAeyjcqavmezy86sWCAeXrgt3HBKa4HA8CZdT8nH",
391 issue.id()?.encoded()
392 );
393 let transfer = &block.transactions[3];
394 assert_eq!(
395 "DBozd2VWYe1FDkrdQnJgvcxh9B6mL872onqpSCjF4a7t",
396 transfer.id()?.encoded()
397 );
398 let burn = &block.transactions[5];
404 assert_eq!(
405 "7Ruo9tnYTuBKTRwbSfG2TLooP4v6pz8SkTx1hvCgfJLU",
406 burn.id()?.encoded()
407 );
408
409 println!("{:#?}", block);
410
411 Ok(())
412 }
413}