exonum_explorer/lib.rs
1// Copyright 2020 The Exonum Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Blockchain explorer allows to get information about blocks and transactions in the blockchain.
16//! It allows to request transactions from a block together with the execution statuses,
17//! iterate over blocks, etc.
18//!
19//! This crate is distinct from the [explorer *service*][explorer-service] crate. While this crate
20//! provides Rust language APIs for retrieving info from the blockchain, the explorer service
21//! translates these APIs into REST and WebSocket endpoints. Correspondingly, this crate is
22//! primarily useful for Rust-language client apps. Another use case is testing; the [testkit]
23//! returns [`BlockWithTransactions`] from its `create_block*` methods and re-exports the entire
24//! crate as `explorer`.
25//!
26//! See the examples in the crate for examples of usage.
27//!
28//! [explorer-service]: https://docs.rs/exonum-explorer-service/
29//! [`BlockWithTransactions`]: struct.BlockWithTransactions.html
30//! [testkit]: https://docs.rs/exonum-testkit/latest/exonum_testkit/struct.TestKit.html
31
32#![warn(
33 missing_debug_implementations,
34 missing_docs,
35 unsafe_code,
36 bare_trait_objects
37)]
38#![warn(clippy::pedantic)]
39#![allow(
40 // Next `cast_*` lints don't give alternatives.
41 clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss,
42 // Next lints produce too much noise/false positives.
43 clippy::module_name_repetitions, clippy::similar_names, clippy::must_use_candidate,
44 clippy::pub_enum_variant_names,
45 // '... may panic' lints.
46 clippy::indexing_slicing,
47 // Too much work to fix.
48 clippy::missing_errors_doc,
49 // False positive: WebSocket
50 clippy::doc_markdown
51)]
52
53use chrono::{DateTime, Utc};
54use exonum::{
55 blockchain::{Block, CallInBlock, CallProof, Schema, TxLocation},
56 crypto::Hash,
57 helpers::Height,
58 merkledb::{ListProof, ObjectHash, Snapshot},
59 messages::{AnyTx, Precommit, Verified},
60 runtime::{ExecutionError, ExecutionStatus},
61};
62use serde::{Serialize, Serializer};
63use serde_derive::*;
64
65use std::{
66 cell::{Ref, RefCell},
67 collections::{BTreeMap, Bound},
68 fmt,
69 ops::{Index, RangeBounds},
70 slice,
71 time::UNIX_EPOCH,
72};
73
74pub mod api;
75
76/// Ending height of the range (exclusive), given the a priori max height.
77fn end_height(bound: Bound<&Height>, max: Height) -> Height {
78 use std::cmp::min;
79
80 let inner_end = match bound {
81 Bound::Included(height) => height.next(),
82 Bound::Excluded(height) => *height,
83 Bound::Unbounded => max.next(),
84 };
85
86 min(inner_end, max.next())
87}
88
89/// Information about a block in the blockchain.
90///
91/// # JSON presentation
92///
93/// JSON object with the following fields:
94///
95/// | Name | Equivalent type | Description |
96/// |------|-------|--------|
97/// | `block` | [`Block`] | Block header as recorded in the blockchain |
98/// | `precommits` | `Vec<`[`Precommit`]`>` | Precommits authorizing the block |
99/// | `txs` | `Vec<`[`Hash`]`>` | Hashes of transactions in the block |
100///
101/// [`Block`]: https://docs.rs/exonum/latest/exonum/blockchain/struct.Block.html
102/// [`Precommit`]: https://docs.rs/exonum/latest/exonum/messages/struct.Precommit.html
103/// [`Hash`]: https://docs.rs/exonum-crypto/latest/exonum_crypto/struct.Hash.html
104#[derive(Debug)]
105pub struct BlockInfo<'a> {
106 header: Block,
107 explorer: &'a BlockchainExplorer<'a>,
108 precommits: RefCell<Option<Vec<Verified<Precommit>>>>,
109 txs: RefCell<Option<Vec<Hash>>>,
110}
111
112impl<'a> BlockInfo<'a> {
113 fn new(explorer: &'a BlockchainExplorer<'_>, height: Height) -> Self {
114 let schema = explorer.schema;
115 let hashes = schema.block_hashes_by_height();
116 let blocks = schema.blocks();
117
118 let block_hash = hashes
119 .get(height.0)
120 .unwrap_or_else(|| panic!("Block not found, height: {:?}", height));
121 let header = blocks
122 .get(&block_hash)
123 .unwrap_or_else(|| panic!("Block not found, hash: {:?}", block_hash));
124
125 BlockInfo {
126 explorer,
127 header,
128 precommits: RefCell::new(None),
129 txs: RefCell::new(None),
130 }
131 }
132
133 /// Returns block header as recorded in the blockchain.
134 pub fn header(&self) -> &Block {
135 &self.header
136 }
137
138 /// Extracts the header discarding all other information.
139 pub fn into_header(self) -> Block {
140 self.header
141 }
142
143 /// Returns the height of this block.
144 ///
145 /// This method is equivalent to calling `block.header().height()`.
146 pub fn height(&self) -> Height {
147 self.header.height
148 }
149
150 /// Returns the number of transactions in this block.
151 pub fn len(&self) -> usize {
152 self.header.tx_count as usize
153 }
154
155 /// Is this block empty (i.e., contains no transactions)?
156 pub fn is_empty(&self) -> bool {
157 self.len() == 0
158 }
159
160 /// Returns a list of precommits for this block.
161 pub fn precommits(&self) -> Ref<'_, [Verified<Precommit>]> {
162 if self.precommits.borrow().is_none() {
163 let precommits = self.explorer.precommits(&self.header);
164 *self.precommits.borrow_mut() = Some(precommits);
165 }
166
167 Ref::map(self.precommits.borrow(), |cache| {
168 cache.as_ref().unwrap().as_ref()
169 })
170 }
171
172 /// Lists hashes of transactions included in this block.
173 pub fn transaction_hashes(&self) -> Ref<'_, [Hash]> {
174 if self.txs.borrow().is_none() {
175 let txs = self.explorer.transaction_hashes(&self.header);
176 *self.txs.borrow_mut() = Some(txs);
177 }
178
179 Ref::map(self.txs.borrow(), |cache| cache.as_ref().unwrap().as_ref())
180 }
181
182 /// Returns a transaction with the specified index in the block.
183 pub fn transaction(&self, index: usize) -> Option<CommittedTransaction> {
184 self.transaction_hashes()
185 .get(index)
186 .map(|hash| self.explorer.committed_transaction(hash, None))
187 }
188
189 /// Returns the proof for the execution status of a call within this block.
190 ///
191 /// Note that if the call did not result in an error or did not happen at all, the returned
192 /// proof will not contain entries. To distinguish between two cases, one can inspect
193 /// the number of transactions in the block or IDs of the active services when the block
194 /// was executed.
195 pub fn call_proof(&self, call_location: CallInBlock) -> CallProof {
196 self.explorer
197 .schema
198 .call_records(self.header.height)
199 .unwrap() // safe: we know that the block exists
200 .get_proof(call_location)
201 }
202
203 /// Iterates over transactions in the block.
204 pub fn iter(&self) -> Transactions<'_, '_> {
205 Transactions {
206 block: self,
207 ptr: 0,
208 len: self.len(),
209 }
210 }
211
212 /// Loads transactions, errors and precommits for the block.
213 pub fn with_transactions(self) -> BlockWithTransactions {
214 let (explorer, header, precommits, transactions) =
215 (self.explorer, self.header, self.precommits, self.txs);
216
217 let precommits = precommits
218 .into_inner()
219 .unwrap_or_else(|| explorer.precommits(&header));
220 let transactions = transactions
221 .into_inner()
222 .unwrap_or_else(|| explorer.transaction_hashes(&header))
223 .iter()
224 .map(|tx_hash| explorer.committed_transaction(tx_hash, None))
225 .collect();
226 let errors = self
227 .explorer
228 .schema
229 .call_records(header.height)
230 .expect("No call record for a committed block");
231 let errors: Vec<_> = errors
232 .errors()
233 .map(|(location, error)| ErrorWithLocation { location, error })
234 .collect();
235
236 BlockWithTransactions {
237 header,
238 precommits,
239 transactions,
240 errors,
241 }
242 }
243}
244
245impl<'a> Serialize for BlockInfo<'a> {
246 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
247 use serde::ser::SerializeStruct;
248
249 let mut s = serializer.serialize_struct("BlockInfo", 3)?;
250 s.serialize_field("block", &self.header)?;
251 s.serialize_field("precommits", &*self.precommits())?;
252 s.serialize_field("txs", &*self.transaction_hashes())?;
253 s.end()
254 }
255}
256
257/// Iterator over transactions in a block.
258#[derive(Debug)]
259pub struct Transactions<'r, 'a> {
260 block: &'r BlockInfo<'a>,
261 ptr: usize,
262 len: usize,
263}
264
265impl<'a, 'r> Iterator for Transactions<'a, 'r> {
266 type Item = CommittedTransaction;
267
268 fn next(&mut self) -> Option<CommittedTransaction> {
269 if self.ptr == self.len {
270 None
271 } else {
272 let transaction = self.block.transaction(self.ptr);
273 self.ptr += 1;
274 transaction
275 }
276 }
277}
278
279impl<'a, 'r: 'a> IntoIterator for &'r BlockInfo<'a> {
280 type Item = CommittedTransaction;
281 type IntoIter = Transactions<'a, 'r>;
282
283 fn into_iter(self) -> Transactions<'a, 'r> {
284 self.iter()
285 }
286}
287
288/// Information about a block in the blockchain with info on transactions eagerly loaded.
289#[derive(Debug, Serialize, Deserialize)]
290#[non_exhaustive]
291pub struct BlockWithTransactions {
292 /// Block header as recorded in the blockchain.
293 #[serde(rename = "block")]
294 pub header: Block,
295 /// Precommits.
296 pub precommits: Vec<Verified<Precommit>>,
297 /// Transactions in the order they appear in the block.
298 pub transactions: Vec<CommittedTransaction>,
299 /// Errors that have occurred within the block.
300 pub errors: Vec<ErrorWithLocation>,
301}
302
303/// Execution error together with its location within the block.
304#[derive(Debug, Serialize, Deserialize)]
305#[non_exhaustive]
306pub struct ErrorWithLocation {
307 /// Location of the error.
308 pub location: CallInBlock,
309 /// Error data.
310 pub error: ExecutionError,
311}
312
313impl fmt::Display for ErrorWithLocation {
314 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(formatter, "In {}: {}", self.location, self.error)
316 }
317}
318
319impl BlockWithTransactions {
320 /// Returns the height of this block.
321 ///
322 /// This method is equivalent to calling `block.header.height()`.
323 pub fn height(&self) -> Height {
324 self.header.height
325 }
326
327 /// Returns the number of transactions in this block.
328 pub fn len(&self) -> usize {
329 self.transactions.len()
330 }
331
332 /// Is this block empty (i.e., contains no transactions)?
333 pub fn is_empty(&self) -> bool {
334 self.transactions.is_empty()
335 }
336
337 /// Iterates over transactions in the block.
338 pub fn iter(&self) -> EagerTransactions<'_> {
339 self.transactions.iter()
340 }
341
342 /// Returns errors converted into a map. Note that this is potentially a costly operation.
343 pub fn error_map(&self) -> BTreeMap<CallInBlock, &ExecutionError> {
344 self.errors.iter().map(|e| (e.location, &e.error)).collect()
345 }
346}
347
348/// Iterator over transactions in [`BlockWithTransactions`].
349///
350/// [`BlockWithTransactions`]: struct.BlockWithTransactions.html
351pub type EagerTransactions<'a> = slice::Iter<'a, CommittedTransaction>;
352
353impl Index<usize> for BlockWithTransactions {
354 type Output = CommittedTransaction;
355
356 fn index(&self, index: usize) -> &CommittedTransaction {
357 self.transactions.get(index).unwrap_or_else(|| {
358 panic!(
359 "Index exceeds number of transactions in block {}",
360 self.len()
361 );
362 })
363 }
364}
365
366/// Returns a transaction in the block by its hash. Beware that this is a slow operation
367/// (linear w.r.t. the number of transactions in a block).
368impl Index<Hash> for BlockWithTransactions {
369 type Output = CommittedTransaction;
370
371 fn index(&self, index: Hash) -> &CommittedTransaction {
372 self.transactions
373 .iter()
374 .find(|&tx| tx.message.object_hash() == index)
375 .unwrap_or_else(|| {
376 panic!("No transaction with hash {} in the block", index);
377 })
378 }
379}
380
381impl<'a> IntoIterator for &'a BlockWithTransactions {
382 type Item = &'a CommittedTransaction;
383 type IntoIter = EagerTransactions<'a>;
384
385 fn into_iter(self) -> EagerTransactions<'a> {
386 self.iter()
387 }
388}
389
390/// Information about a particular transaction in the blockchain.
391///
392/// # JSON presentation
393///
394/// | Name | Equivalent type | Description |
395/// |------|-------|--------|
396/// | `message` | `Verified<AnyTx>` | Transaction as recorded in the blockchain |
397/// | `location` | [`TxLocation`] | Location of the transaction in the block |
398/// | `location_proof` | [`ListProof`]`<`[`Hash`]`>` | Proof of transaction inclusion into a block |
399/// | `status` | (custom; see below) | Execution status |
400/// | `time` | [`DateTime`]`<`[`Utc`]`>` | Commitment time* |
401///
402/// \* By commitment time we mean an approximate commitment time of the block
403/// which includes the transaction. This time is a median time of the precommit local times
404/// of each validator.
405///
406/// ## `status` field
407///
408/// The `status` field is a more readable representation of the [`ExecutionStatus`] type.
409///
410/// For successfully executed transactions, `status` is equal to
411///
412/// ```json
413/// { "type": "success" }
414/// ```
415///
416/// For transactions that cause an [`ExecutionError`], `status` contains the error code
417/// and an optional description, i.e., has the following type in the [TypeScript] notation:
418///
419/// ```typescript
420/// type Error = {
421/// type: 'service_error' | 'core_error' | 'common_error' | 'runtime_error' | 'unexpected_error',
422/// code?: number,
423/// description?: string,
424/// runtime_id: number,
425/// call_site?: CallSite,
426/// };
427///
428/// type CallSite = MethodCallSite | HookCallSite;
429///
430/// type MethodCallSite = {
431/// call_type: 'method',
432/// instance_id: number,
433/// interface?: string,
434/// method_id: number,
435/// };
436///
437/// type HookCallSite = {
438/// call_type: 'constructor' | 'before_transactions' | 'after_transactions',
439/// instance_id: number,
440/// };
441/// ```
442///
443/// Explanations:
444///
445/// - `Error.type` determines the component responsible for the error. Usually, errors
446/// are generated by the service code, but they can also be caused by the dispatch logic,
447/// runtime associated with the service, or come from another source (`unexpected_error`s).
448/// - `Error.code` is the error code. For service errors, this code is specific
449/// to the service instance (which can be obtained from `call_site`), and for runtime errors -
450/// to the runtime. For core errors, the codes are fixed; their meaning can be found
451/// in the [`CoreError`] docs. The code is present for all error types except
452/// `unexpected_error`s, in which the code is always absent.
453/// Besides types listed above, there is also a set of errors that can occur within any context,
454/// which are organized in the [`CommonError`].
455/// - `Error.description` is an optional human-readable description of the error.
456/// - `Error.runtime_id` is the numeric ID of the runtime in which the error has occurred. Note
457/// that the runtime is defined for all error types, not just `runtime_error`s, since
458/// for any request it's possible to say which runtime is responsible for its processing.
459/// - `Error.call_site` provides most precise known location of the call in which the error
460/// has occurred.
461///
462/// [`TxLocation`]: https://docs.rs/exonum/latest/exonum/blockchain/struct.TxLocation.html
463/// [`ListProof`]: https://docs.rs/exonum-merkledb/latest/exonum_merkledb/indexes/proof_list/struct.ListProof.html
464/// [`Hash`]: https://docs.rs/exonum-crypto/latest/exonum_crypto/struct.Hash.html
465/// [`ExecutionStatus`]: https://docs.rs/exonum/latest/exonum/runtime/struct.ExecutionStatus.html
466/// [`ExecutionError`]: https://docs.rs/exonum/latest/exonum/runtime/struct.ExecutionError.html
467/// [`CoreError`]: https://docs.rs/exonum/latest/exonum/runtime/enum.CoreError.html
468/// [`CommonError`]: https://docs.rs/exonum/latest/exonum/runtime/enum.CommonError.html
469/// [TypeScript]: https://www.typescriptlang.org/
470/// [`DateTime`]: https://docs.rs/chrono/0.4.10/chrono/struct.DateTime.html
471/// [`Utc`]: https://docs.rs/chrono/0.4.10/chrono/offset/struct.Utc.html
472#[derive(Debug, Serialize, Deserialize)]
473pub struct CommittedTransaction {
474 message: Verified<AnyTx>,
475 location: TxLocation,
476 location_proof: ListProof<Hash>,
477 status: ExecutionStatus,
478 time: DateTime<Utc>,
479}
480
481impl CommittedTransaction {
482 /// Returns the content of the transaction.
483 pub fn message(&self) -> &Verified<AnyTx> {
484 &self.message
485 }
486
487 /// Returns the transaction location in block.
488 pub fn location(&self) -> &TxLocation {
489 &self.location
490 }
491
492 /// Returns a proof that transaction is recorded in the blockchain.
493 pub fn location_proof(&self) -> &ListProof<Hash> {
494 &self.location_proof
495 }
496
497 /// Returns the status of the transaction execution.
498 pub fn status(&self) -> Result<(), &ExecutionError> {
499 self.status.0.as_ref().map(drop)
500 }
501
502 /// Returns an approximate commit time of the block which includes this transaction.
503 pub fn time(&self) -> &DateTime<Utc> {
504 &self.time
505 }
506}
507
508/// Information about the transaction.
509///
510/// Values of this type are returned by the `transaction()` method of the `BlockchainExplorer`.
511///
512/// # JSON presentation
513///
514/// ## Committed transactions
515///
516/// Committed transactions are represented just like a `CommittedTransaction`,
517/// with the additional `type` field equal to `"committed"`.
518///
519/// ## Transaction in pool
520///
521/// Transactions in pool are represented with a 2-field object:
522///
523/// - `type` field contains transaction type (`"in-pool"`).
524/// - `message` is the full transaction message serialized to the hexadecimal form.
525///
526/// # Examples
527///
528/// ```
529/// use exonum_explorer::TransactionInfo;
530/// use exonum::{crypto::KeyPair, runtime::InstanceId};
531/// # use exonum_derive::*;
532/// # use serde_derive::*;
533/// # use serde_json::json;
534///
535/// /// Service interface.
536/// #[exonum_interface]
537/// trait ServiceInterface<Ctx> {
538/// type Output;
539/// #[interface_method(id = 0)]
540/// fn create_wallet(&self, ctx: Ctx, username: String) -> Self::Output;
541/// }
542///
543/// // Create a signed transaction.
544/// let keypair = KeyPair::random();
545/// const SERVICE_ID: InstanceId = 100;
546/// let tx = keypair.create_wallet(SERVICE_ID, "Alice".to_owned());
547/// // This transaction in pool will be represented as follows:
548/// let json = json!({
549/// "type": "in_pool",
550/// "message": tx,
551/// });
552/// let parsed: TransactionInfo = serde_json::from_value(json).unwrap();
553/// assert!(parsed.is_in_pool());
554/// ```
555#[derive(Debug, Serialize, Deserialize)]
556#[serde(tag = "type", rename_all = "snake_case")]
557#[non_exhaustive]
558pub enum TransactionInfo {
559 /// Transaction is in the memory pool, but not yet committed to the blockchain.
560 InPool {
561 /// A content of the uncommitted transaction.
562 message: Verified<AnyTx>,
563 },
564
565 /// Transaction is already committed to the blockchain.
566 Committed(CommittedTransaction),
567}
568
569impl TransactionInfo {
570 /// Returns the content of this transaction.
571 pub fn message(&self) -> &Verified<AnyTx> {
572 match *self {
573 TransactionInfo::InPool { ref message } => message,
574 TransactionInfo::Committed(ref tx) => tx.message(),
575 }
576 }
577
578 /// Is this in-pool transaction?
579 pub fn is_in_pool(&self) -> bool {
580 match *self {
581 TransactionInfo::InPool { .. } => true,
582 _ => false,
583 }
584 }
585
586 /// Is this a committed transaction?
587 pub fn is_committed(&self) -> bool {
588 match *self {
589 TransactionInfo::Committed(_) => true,
590 _ => false,
591 }
592 }
593
594 /// Returns a reference to the inner committed transaction if this transaction is committed.
595 /// For transactions in pool, returns `None`.
596 pub fn as_committed(&self) -> Option<&CommittedTransaction> {
597 match *self {
598 TransactionInfo::Committed(ref tx) => Some(tx),
599 _ => None,
600 }
601 }
602}
603
604/// Blockchain explorer.
605///
606/// # Notes
607///
608/// The explorer wraps a specific [`Snapshot`] of the blockchain state; that is,
609/// all calls to the methods of an explorer instance are guaranteed to be consistent.
610///
611/// [`Snapshot`]: https://docs.rs/exonum-merkledb/latest/exonum_merkledb/trait.Snapshot.html
612#[derive(Debug, Copy, Clone)]
613pub struct BlockchainExplorer<'a> {
614 schema: Schema<&'a dyn Snapshot>,
615}
616
617impl<'a> BlockchainExplorer<'a> {
618 /// Creates a new `BlockchainExplorer` instance from the provided snapshot.
619 pub fn new(snapshot: &'a dyn Snapshot) -> Self {
620 BlockchainExplorer {
621 schema: Schema::new(snapshot),
622 }
623 }
624
625 /// Creates a new `BlockchainExplorer` instance from the core schema.
626 pub fn from_schema(schema: Schema<&'a dyn Snapshot>) -> Self {
627 BlockchainExplorer { schema }
628 }
629
630 /// Returns information about the transaction identified by the hash.
631 pub fn transaction(&self, tx_hash: &Hash) -> Option<TransactionInfo> {
632 let message = self.transaction_without_proof(tx_hash)?;
633 if self.schema.transactions_pool().contains(tx_hash) {
634 return Some(TransactionInfo::InPool { message });
635 }
636
637 let tx = self.committed_transaction(tx_hash, Some(message));
638 Some(TransactionInfo::Committed(tx))
639 }
640
641 /// Returns the status of a call in a block.
642 ///
643 /// # Return value
644 ///
645 /// This method will return `Ok(())` both if the call completed successfully, or if
646 /// was not performed at all. The caller is responsible to distinguish these two outcomes.
647 pub fn call_status(
648 &self,
649 block_height: Height,
650 call_location: CallInBlock,
651 ) -> Result<(), ExecutionError> {
652 match self.schema.call_records(block_height) {
653 Some(errors) => errors.get(call_location),
654 None => Ok(()),
655 }
656 }
657
658 /// Return transaction message without proof.
659 pub fn transaction_without_proof(&self, tx_hash: &Hash) -> Option<Verified<AnyTx>> {
660 self.schema.transactions().get(tx_hash)
661 }
662
663 fn precommits(&self, block: &Block) -> Vec<Verified<Precommit>> {
664 self.schema
665 .precommits(&block.object_hash())
666 .iter()
667 .collect()
668 }
669
670 fn transaction_hashes(&self, block: &Block) -> Vec<Hash> {
671 let tx_hashes_table = self.schema.block_transactions(block.height);
672 tx_hashes_table.iter().collect()
673 }
674
675 /// Retrieves a transaction that is known to be committed.
676 fn committed_transaction(
677 &self,
678 tx_hash: &Hash,
679 maybe_content: Option<Verified<AnyTx>>,
680 ) -> CommittedTransaction {
681 let location = self
682 .schema
683 .transactions_locations()
684 .get(tx_hash)
685 .unwrap_or_else(|| panic!("Location not found for transaction hash {:?}", tx_hash));
686
687 let location_proof = self
688 .schema
689 .block_transactions(location.block_height())
690 .get_proof(u64::from(location.position_in_block()));
691
692 let block_precommits = self
693 .schema
694 .block_and_precommits(location.block_height())
695 .unwrap();
696 let time = median_precommits_time(&block_precommits.precommits);
697
698 // Unwrap is OK here, because we already know that transaction is committed.
699 let status = self.schema.transaction_result(location).unwrap();
700
701 CommittedTransaction {
702 message: maybe_content.unwrap_or_else(|| {
703 self.schema
704 .transactions()
705 .get(tx_hash)
706 .expect("BUG: Cannot find transaction in database")
707 }),
708 location,
709 location_proof,
710 status: ExecutionStatus(status),
711 time,
712 }
713 }
714
715 /// Return the height of the blockchain.
716 pub fn height(&self) -> Height {
717 self.schema.height()
718 }
719
720 /// Returns block information for the specified height or `None` if there is no such block.
721 pub fn block(&self, height: Height) -> Option<BlockInfo<'_>> {
722 if self.height() >= height {
723 Some(BlockInfo::new(self, height))
724 } else {
725 None
726 }
727 }
728
729 /// Return a block together with its transactions at the specified height, or `None`
730 /// if there is no such block.
731 pub fn block_with_txs(&self, height: Height) -> Option<BlockWithTransactions> {
732 let txs_table = self.schema.block_transactions(height);
733 let block_proof = self.schema.block_and_precommits(height)?;
734 let errors = self.schema.call_records(height)?;
735
736 Some(BlockWithTransactions {
737 header: block_proof.block,
738 precommits: block_proof.precommits,
739 transactions: txs_table
740 .iter()
741 .map(|tx_hash| self.committed_transaction(&tx_hash, None))
742 .collect(),
743 errors: errors
744 .errors()
745 .map(|(location, error)| ErrorWithLocation { location, error })
746 .collect(),
747 })
748 }
749
750 /// Iterates over blocks in the blockchain.
751 pub fn blocks<R: RangeBounds<Height>>(&self, heights: R) -> Blocks<'_> {
752 use std::cmp::max;
753
754 let max_height = self.schema.height();
755 let ptr = match heights.start_bound() {
756 Bound::Included(height) => *height,
757 Bound::Excluded(height) => height.next(),
758 Bound::Unbounded => Height(0),
759 };
760 Blocks {
761 explorer: self,
762 ptr,
763 back: max(ptr, end_height(heights.end_bound(), max_height)),
764 }
765 }
766}
767
768/// Iterator over blocks in the blockchain.
769pub struct Blocks<'a> {
770 explorer: &'a BlockchainExplorer<'a>,
771 ptr: Height,
772 back: Height,
773}
774
775impl<'a> fmt::Debug for Blocks<'a> {
776 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
777 formatter
778 .debug_struct("Blocks")
779 .field("ptr", &self.ptr)
780 .field("back", &self.back)
781 .finish()
782 }
783}
784
785impl<'a> Iterator for Blocks<'a> {
786 type Item = BlockInfo<'a>;
787
788 fn next(&mut self) -> Option<BlockInfo<'a>> {
789 if self.ptr == self.back {
790 return None;
791 }
792
793 let block = BlockInfo::new(self.explorer, self.ptr);
794 self.ptr = self.ptr.next();
795 Some(block)
796 }
797
798 fn size_hint(&self) -> (usize, Option<usize>) {
799 let exact = (self.back.0 - self.ptr.0) as usize;
800 (exact, Some(exact))
801 }
802
803 fn count(self) -> usize {
804 (self.back.0 - self.ptr.0) as usize
805 }
806
807 fn nth(&mut self, n: usize) -> Option<BlockInfo<'a>> {
808 if self.ptr.0 + n as u64 >= self.back.0 {
809 self.ptr = self.back;
810 None
811 } else {
812 self.ptr = Height(self.ptr.0 + n as u64);
813 let block = BlockInfo::new(self.explorer, self.ptr);
814 self.ptr = self.ptr.next();
815 Some(block)
816 }
817 }
818}
819
820impl<'a> DoubleEndedIterator for Blocks<'a> {
821 fn next_back(&mut self) -> Option<BlockInfo<'a>> {
822 if self.ptr == self.back {
823 return None;
824 }
825
826 self.back = self.back.previous();
827 Some(BlockInfo::new(self.explorer, self.back))
828 }
829}
830
831/// Calculates a median time from precommits.
832pub fn median_precommits_time(precommits: &[Verified<Precommit>]) -> DateTime<Utc> {
833 if precommits.is_empty() {
834 UNIX_EPOCH.into()
835 } else {
836 let mut times: Vec<_> = precommits.iter().map(|p| p.payload().time).collect();
837 times.sort();
838 times[times.len() / 2]
839 }
840}