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}