hotshot_query_service/data_source/storage/sql/queries/
explorer.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the HotShot Query Service library.
3//
4// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
5// General Public License as published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
8// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// General Public License for more details.
10// You should have received a copy of the GNU General Public License along with this program. If not,
11// see <https://www.gnu.org/licenses/>.
12
13//! Explorer storage implementation for a database query engine.
14
15use std::{collections::VecDeque, num::NonZeroUsize};
16
17use async_trait::async_trait;
18use committable::{Commitment, Committable};
19use futures::stream::{self, StreamExt, TryStreamExt};
20use hotshot_types::traits::{block_contents::BlockHeader, node_implementation::NodeType};
21use itertools::Itertools;
22use sqlx::{FromRow, Row};
23use tagged_base64::{Tagged, TaggedBase64};
24
25use super::{
26    super::transaction::{Transaction, TransactionMode, query},
27    BLOCK_COLUMNS, Database, Db, DecodeError,
28};
29use crate::{
30    Header, Payload, QueryError, QueryResult, Transaction as HotshotTransaction,
31    availability::{BlockQueryData, QueryableHeader, QueryablePayload},
32    data_source::storage::{ExplorerStorage, NodeStorage},
33    explorer::{
34        self, BalanceAmount, BlockDetail, BlockIdentifier, BlockRange, BlockSummary,
35        ExplorerHistograms, ExplorerSummary, GenesisOverview, GetBlockDetailError,
36        GetBlockSummariesError, GetBlockSummariesRequest, GetExplorerSummaryError,
37        GetSearchResultsError, GetTransactionDetailError, GetTransactionSummariesError,
38        GetTransactionSummariesRequest, MonetaryValue, SearchResult, TransactionIdentifier,
39        TransactionRange, TransactionSummary, TransactionSummaryFilter,
40        errors::{self, NotFound},
41        query_data::TransactionDetailResponse,
42        traits::ExplorerHeader,
43    },
44    types::HeightIndexed,
45};
46
47impl From<sqlx::Error> for GetExplorerSummaryError {
48    fn from(err: sqlx::Error) -> Self {
49        Self::from(QueryError::from(err))
50    }
51}
52
53impl From<sqlx::Error> for GetTransactionDetailError {
54    fn from(err: sqlx::Error) -> Self {
55        Self::from(QueryError::from(err))
56    }
57}
58
59impl From<sqlx::Error> for GetTransactionSummariesError {
60    fn from(err: sqlx::Error) -> Self {
61        Self::from(QueryError::from(err))
62    }
63}
64
65impl From<sqlx::Error> for GetBlockDetailError {
66    fn from(err: sqlx::Error) -> Self {
67        Self::from(QueryError::from(err))
68    }
69}
70
71impl From<sqlx::Error> for GetBlockSummariesError {
72    fn from(err: sqlx::Error) -> Self {
73        Self::from(QueryError::from(err))
74    }
75}
76
77impl From<sqlx::Error> for GetSearchResultsError {
78    fn from(err: sqlx::Error) -> Self {
79        Self::from(QueryError::from(err))
80    }
81}
82
83impl<'r, Types> FromRow<'r, <Db as Database>::Row> for BlockSummary<Types>
84where
85    Types: NodeType,
86    Header<Types>: BlockHeader<Types> + ExplorerHeader<Types>,
87    Payload<Types>: QueryablePayload<Types>,
88{
89    fn from_row(row: &'r <Db as Database>::Row) -> sqlx::Result<Self> {
90        BlockQueryData::<Types>::from_row(row)?
91            .try_into()
92            .decode_error("malformed block summary")
93    }
94}
95
96impl<'r, Types> FromRow<'r, <Db as Database>::Row> for BlockDetail<Types>
97where
98    Types: NodeType,
99    Header<Types>: BlockHeader<Types> + ExplorerHeader<Types>,
100    Payload<Types>: QueryablePayload<Types>,
101    BalanceAmount<Types>: Into<MonetaryValue>,
102{
103    fn from_row(row: &'r <Db as Database>::Row) -> sqlx::Result<Self> {
104        BlockQueryData::<Types>::from_row(row)?
105            .try_into()
106            .decode_error("malformed block detail")
107    }
108}
109
110lazy_static::lazy_static! {
111    static ref GET_BLOCK_SUMMARIES_QUERY_FOR_LATEST: String = {
112        format!(
113            "SELECT {BLOCK_COLUMNS}
114                FROM header AS h
115                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
116                ORDER BY h.height DESC
117                LIMIT $1"
118            )
119    };
120
121    static ref GET_BLOCK_SUMMARIES_QUERY_FOR_HEIGHT: String = {
122        format!(
123            "SELECT {BLOCK_COLUMNS}
124                FROM header AS h
125                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
126                WHERE h.height <= $1
127                ORDER BY h.height DESC
128                LIMIT $2"
129        )
130    };
131
132    // We want to match the blocks starting with the given hash, and working backwards
133    // until we have returned up to the number of requested blocks.  The hash for a
134    // block should be unique, so we should just need to start with identifying the
135    // block height with the given hash, and return all blocks with a height less than
136    // or equal to that height, up to the number of requested blocks.
137    static ref GET_BLOCK_SUMMARIES_QUERY_FOR_HASH: String = {
138        format!(
139            "SELECT {BLOCK_COLUMNS}
140                FROM header AS h
141                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
142                WHERE h.height <= (SELECT h1.height FROM header AS h1 WHERE h1.hash = $1)
143                ORDER BY h.height DESC
144                LIMIT $2",
145        )
146    };
147
148    static ref GET_BLOCK_DETAIL_QUERY_FOR_LATEST: String = {
149        format!(
150            "SELECT {BLOCK_COLUMNS}
151                FROM header AS h
152                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
153                ORDER BY h.height DESC
154                LIMIT 1"
155        )
156    };
157
158    static ref GET_BLOCK_DETAIL_QUERY_FOR_HEIGHT: String = {
159        format!(
160            "SELECT {BLOCK_COLUMNS}
161                FROM header AS h
162                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
163                WHERE h.height = $1
164                ORDER BY h.height DESC
165                LIMIT 1"
166        )
167    };
168
169    static ref GET_BLOCK_DETAIL_QUERY_FOR_HASH: String = {
170        format!(
171            "SELECT {BLOCK_COLUMNS}
172                FROM header AS h
173                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
174                WHERE h.hash = $1
175                ORDER BY h.height DESC
176                LIMIT 1"
177        )
178    };
179
180
181    static ref GET_BLOCKS_CONTAINING_TRANSACTIONS_NO_FILTER_QUERY: String = {
182        format!(
183            "SELECT {BLOCK_COLUMNS}
184               FROM header AS h
185               JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
186               WHERE h.height IN (
187                   SELECT t.block_height
188                       FROM transactions AS t
189                       WHERE (t.block_height, t.ns_id, t.position) <= ($1, $2, $3)
190                       ORDER BY t.block_height DESC, t.ns_id DESC, t.position DESC
191                       LIMIT $4
192               )
193               ORDER BY h.height DESC"
194        )
195    };
196
197    static ref GET_BLOCKS_CONTAINING_TRANSACTIONS_IN_NAMESPACE_QUERY: String = {
198        format!(
199            "SELECT {BLOCK_COLUMNS}
200               FROM header AS h
201               JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
202               WHERE h.height IN (
203                   SELECT t.block_height
204                       FROM transactions AS t
205                       WHERE (t.block_height, t.ns_id, t.position) <= ($1, $2, $3)
206                         AND t.ns_id = $5
207                       ORDER BY t.block_height DESC, t.ns_id DESC, t.position DESC
208                       LIMIT $4
209               )
210               ORDER BY h.height DESC"
211        )
212    };
213
214    static ref GET_TRANSACTION_SUMMARIES_QUERY_FOR_BLOCK: String = {
215        format!(
216            "SELECT {BLOCK_COLUMNS}
217                FROM header AS h
218                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
219                WHERE  h.height = $1
220                ORDER BY h.height DESC"
221        )
222    };
223
224    static ref GET_TRANSACTION_DETAIL_QUERY_FOR_LATEST: String = {
225        format!(
226            "SELECT {BLOCK_COLUMNS}
227                FROM header AS h
228                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
229                WHERE h.height = (
230                    SELECT MAX(t1.block_height)
231                        FROM transactions AS t1
232                )
233                ORDER BY h.height DESC"
234        )
235    };
236
237    static ref GET_TRANSACTION_DETAIL_QUERY_FOR_HEIGHT_AND_OFFSET: String = {
238        format!(
239            "SELECT {BLOCK_COLUMNS}
240                FROM header AS h
241                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
242                WHERE h.height = (
243                    SELECT t1.block_height
244                        FROM transactions AS t1
245                        WHERE t1.block_height = $1
246                        ORDER BY t1.block_height, t1.ns_id, t1.position
247                        LIMIT 1
248                        OFFSET $2
249                       
250                )
251                ORDER BY h.height DESC",
252        )
253    };
254
255    static ref GET_TRANSACTION_DETAIL_QUERY_FOR_HASH: String = {
256        format!(
257            "SELECT {BLOCK_COLUMNS}
258                FROM header AS h
259                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
260                WHERE h.height = (
261                    SELECT t1.block_height
262                        FROM transactions AS t1
263                        WHERE t1.hash = $1
264                        ORDER BY t1.block_height DESC, t1.ns_id DESC, t1.position DESC
265                        LIMIT 1
266                )
267                ORDER BY h.height DESC"
268        )
269    };
270}
271
272/// [EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES] is the number of entries we want
273/// to return in our histogram summary.
274const EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES: usize = 50;
275
276/// [EXPLORER_SUMMARY_NUM_BLOCKS] is the number of blocks we want to return in
277/// our explorer summary.
278const EXPLORER_SUMMARY_NUM_BLOCKS: usize = 10;
279
280/// [EXPLORER_SUMMARY_NUM_TRANSACTIONS] is the number of transactions we want
281/// to return in our explorer summary.
282const EXPLORER_SUMMARY_NUM_TRANSACTIONS: usize = 10;
283
284/// MILLIS_PER_UNIT is helper constant that is utilized to aid in the
285/// conversion from milli prefix SI units to the uniary unit type.
286const MILLIS_PER_UNIT: f64 = 1_000.0;
287
288#[async_trait]
289impl<Mode, Types> ExplorerStorage<Types> for Transaction<Mode>
290where
291    Mode: TransactionMode,
292    Types: NodeType,
293    Payload<Types>: QueryablePayload<Types>,
294    Header<Types>: QueryableHeader<Types> + ExplorerHeader<Types>,
295    crate::Transaction<Types>: explorer::traits::ExplorerTransaction<Types>,
296    BalanceAmount<Types>: Into<explorer::monetary_value::MonetaryValue>,
297{
298    async fn get_block_summaries(
299        &mut self,
300        request: GetBlockSummariesRequest<Types>,
301    ) -> Result<Vec<BlockSummary<Types>>, GetBlockSummariesError> {
302        let request = &request.0;
303
304        let query_stmt = match request.target {
305            BlockIdentifier::Latest => {
306                query(&GET_BLOCK_SUMMARIES_QUERY_FOR_LATEST).bind(request.num_blocks.get() as i64)
307            },
308            BlockIdentifier::Height(height) => query(&GET_BLOCK_SUMMARIES_QUERY_FOR_HEIGHT)
309                .bind(height as i64)
310                .bind(request.num_blocks.get() as i64),
311            BlockIdentifier::Hash(hash) => query(&GET_BLOCK_SUMMARIES_QUERY_FOR_HASH)
312                .bind(hash.to_string())
313                .bind(request.num_blocks.get() as i64),
314        };
315
316        let row_stream = query_stmt.fetch(self.as_mut());
317        let result = row_stream.map(|row| BlockSummary::from_row(&row?));
318
319        Ok(result.try_collect().await?)
320    }
321
322    async fn get_block_detail(
323        &mut self,
324        request: BlockIdentifier<Types>,
325    ) -> Result<BlockDetail<Types>, GetBlockDetailError> {
326        let query_stmt = match request {
327            BlockIdentifier::Latest => query(&GET_BLOCK_DETAIL_QUERY_FOR_LATEST),
328            BlockIdentifier::Height(height) => {
329                query(&GET_BLOCK_DETAIL_QUERY_FOR_HEIGHT).bind(height as i64)
330            },
331            BlockIdentifier::Hash(hash) => {
332                query(&GET_BLOCK_DETAIL_QUERY_FOR_HASH).bind(hash.to_string())
333            },
334        };
335
336        let query_result = query_stmt.fetch_one(self.as_mut()).await?;
337        let block = BlockDetail::from_row(&query_result)?;
338
339        Ok(block)
340    }
341
342    async fn get_transaction_summaries(
343        &mut self,
344        request: GetTransactionSummariesRequest<Types>,
345    ) -> Result<Vec<TransactionSummary<Types>>, GetTransactionSummariesError> {
346        let range = &request.range;
347        let target = &range.target;
348        let filter = &request.filter;
349
350        // We need to figure out the transaction target we are going to start
351        // returned results based on.
352        let transaction_target_query = match target {
353            TransactionIdentifier::Latest => query(
354                "SELECT block_height AS height, ns_id, position FROM transactions ORDER BY \
355                 block_height DESC, ns_id DESC, position DESC LIMIT 1",
356            ),
357            TransactionIdentifier::HeightAndOffset(height, _) => query(
358                "SELECT block_height AS height, ns_id, position FROM transactions WHERE \
359                 block_height = $1 ORDER BY ns_id DESC, position DESC LIMIT 1",
360            )
361            .bind(*height as i64),
362            TransactionIdentifier::Hash(hash) => query(
363                "SELECT block_height AS height, ns_id, position FROM transactions WHERE hash = $1 \
364                 ORDER BY block_height DESC, ns_id DESC, position DESC LIMIT 1",
365            )
366            .bind(hash.to_string()),
367        };
368        let Some(transaction_target) = transaction_target_query
369            .fetch_optional(self.as_mut())
370            .await?
371        else {
372            // If nothing is found, then we want to return an empty summary list as it means there
373            // is either no transaction, or the targeting criteria fails to identify any transaction
374            return Ok(vec![]);
375        };
376
377        let block_height = transaction_target.get::<i64, _>("height") as usize;
378        let namespace = transaction_target.get::<i64, _>("ns_id");
379        let position = transaction_target.get::<i64, _>("position");
380        let offset = if let TransactionIdentifier::HeightAndOffset(_, offset) = target {
381            *offset
382        } else {
383            0
384        };
385
386        // Our block_stream is more-or-less always the same, the only difference
387        // is a an additional filter on the identified transactions being found
388        // In general, we use our `transaction_target` to identify the starting
389        // `block_height` and `namespace`, and `position`, and we grab up to `limit`
390        // transactions from that point.  We then grab only the blocks for those
391        // identified transactions, as only those blocks are needed to pull all
392        // of the relevant transactions.
393        let query_stmt = match filter {
394            TransactionSummaryFilter::RollUp(ns) => {
395                query(&GET_BLOCKS_CONTAINING_TRANSACTIONS_IN_NAMESPACE_QUERY)
396                    .bind(block_height as i64)
397                    .bind(namespace)
398                    .bind(position)
399                    .bind((range.num_transactions.get() + offset) as i64)
400                    .bind((*ns).into())
401            },
402            TransactionSummaryFilter::None => {
403                query(&GET_BLOCKS_CONTAINING_TRANSACTIONS_NO_FILTER_QUERY)
404                    .bind(block_height as i64)
405                    .bind(namespace)
406                    .bind(position)
407                    .bind((range.num_transactions.get() + offset) as i64)
408            },
409
410            TransactionSummaryFilter::Block(block) => {
411                query(&GET_TRANSACTION_SUMMARIES_QUERY_FOR_BLOCK).bind(*block as i64)
412            },
413        };
414
415        let block_stream = query_stmt
416            .fetch(self.as_mut())
417            .map(|row| BlockQueryData::from_row(&row?));
418
419        let transaction_summary_stream = block_stream.flat_map(|row| match row {
420            Ok(block) => {
421                tracing::info!(height = block.height(), "selected block");
422                stream::iter(
423                    block
424                        .enumerate()
425                        .filter(|(ix, _)| {
426                            if let TransactionSummaryFilter::RollUp(ns) = filter {
427                                let tx_ns = QueryableHeader::<Types>::namespace_id(
428                                    block.header(),
429                                    &ix.ns_index,
430                                );
431                                tx_ns.as_ref() == Some(ns)
432                            } else {
433                                true
434                            }
435                        })
436                        .enumerate()
437                        .map(|(index, (_, txn))| {
438                            TransactionSummary::try_from((&block, index, txn)).map_err(|err| {
439                                QueryError::Error {
440                                    message: err.to_string(),
441                                }
442                            })
443                        })
444                        .collect::<Vec<QueryResult<TransactionSummary<Types>>>>()
445                        .into_iter()
446                        .rev()
447                        .collect::<Vec<QueryResult<TransactionSummary<Types>>>>(),
448                )
449            },
450            Err(err) => stream::iter(vec![Err(err.into())]),
451        });
452
453        let transaction_summary_vec = transaction_summary_stream
454            .try_collect::<Vec<TransactionSummary<Types>>>()
455            .await?;
456
457        Ok(transaction_summary_vec
458            .into_iter()
459            .skip(offset)
460            .skip_while(|txn| {
461                if let TransactionIdentifier::Hash(hash) = target {
462                    txn.hash != *hash
463                } else {
464                    false
465                }
466            })
467            .take(range.num_transactions.get())
468            .collect::<Vec<TransactionSummary<Types>>>())
469    }
470
471    async fn get_transaction_detail(
472        &mut self,
473        request: TransactionIdentifier<Types>,
474    ) -> Result<TransactionDetailResponse<Types>, GetTransactionDetailError> {
475        let target = request;
476
477        let query_stmt = match target {
478            TransactionIdentifier::Latest => query(&GET_TRANSACTION_DETAIL_QUERY_FOR_LATEST),
479            TransactionIdentifier::HeightAndOffset(height, offset) => {
480                query(&GET_TRANSACTION_DETAIL_QUERY_FOR_HEIGHT_AND_OFFSET)
481                    .bind(height as i64)
482                    .bind(offset as i64)
483            },
484            TransactionIdentifier::Hash(hash) => {
485                query(&GET_TRANSACTION_DETAIL_QUERY_FOR_HASH).bind(hash.to_string())
486            },
487        };
488
489        let query_row = query_stmt.fetch_one(self.as_mut()).await?;
490        let block = BlockQueryData::<Types>::from_row(&query_row)?;
491
492        let txns = block.enumerate().map(|(_, txn)| txn).collect::<Vec<_>>();
493
494        let (offset, txn) = match target {
495            TransactionIdentifier::Latest => txns.into_iter().enumerate().next_back().ok_or(
496                GetTransactionDetailError::TransactionNotFound(NotFound {
497                    key: "Latest".to_string(),
498                }),
499            ),
500            TransactionIdentifier::HeightAndOffset(height, offset) => {
501                txns.into_iter().enumerate().nth(offset).ok_or(
502                    GetTransactionDetailError::TransactionNotFound(NotFound {
503                        key: format!("at {height} and {offset}"),
504                    }),
505                )
506            },
507            TransactionIdentifier::Hash(hash) => txns
508                .into_iter()
509                .enumerate()
510                .find(|(_, txn)| txn.commit() == hash)
511                .ok_or(GetTransactionDetailError::TransactionNotFound(NotFound {
512                    key: format!("hash {hash}"),
513                })),
514        }?;
515
516        Ok(TransactionDetailResponse::try_from((&block, offset, txn))?)
517    }
518
519    async fn get_explorer_summary(
520        &mut self,
521    ) -> Result<ExplorerSummary<Types>, GetExplorerSummaryError> {
522        let histograms = {
523            let histogram_query_result = query(
524                "SELECT
525                    h.height AS height,
526                    h.timestamp AS timestamp,
527                    COALESCE(
528                        CAST(h.data -> 'fields' ->> 'timestamp_millis' AS BIGINT),
529                        CAST(h.data -> 'fields' ->> 'timestamp' AS BIGINT) * 1000
530                    ) - LEAD(COALESCE(
531                        CAST(h.data -> 'fields' ->> 'timestamp_millis' AS BIGINT), 
532                        CAST(h.data -> 'fields' ->> 'timestamp' AS BIGINT) * 1000
533                    )) OVER (ORDER BY h.height DESC) as time,
534                    p.size AS size,
535                    p.num_transactions AS transactions
536                FROM header AS h
537                JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
538                WHERE
539                    h.height IN (SELECT height FROM header ORDER BY height DESC LIMIT $1)
540                ORDER BY h.height
541                ",
542            )
543            .bind((EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES + 1) as i64)
544            .fetch(self.as_mut());
545
546            let mut histograms: ExplorerHistograms = histogram_query_result
547                .map(|row_stream| {
548                    row_stream.map(|row| {
549                        let height: i64 = row.try_get("height")?;
550                        let timestamp: i64 = row.try_get("timestamp")?;
551                        let time: Option<i64> = row.try_get("time")?;
552                        let size: Option<i32> = row.try_get("size")?;
553                        let num_transactions: i32 = row.try_get("transactions")?;
554
555                        Ok((height, timestamp, time, size, num_transactions))
556                    })
557                })
558                .try_fold(
559                    ExplorerHistograms {
560                        block_time: VecDeque::with_capacity(EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES),
561                        block_size: VecDeque::with_capacity(EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES),
562                        block_transactions: VecDeque::with_capacity(EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES),
563                        block_heights: VecDeque::with_capacity(EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES),
564                    },
565                    |mut histograms: ExplorerHistograms,
566                     row: sqlx::Result<(i64, i64, Option<i64>, Option<i32>, i32)>| async {
567                        let (height, _timestamp, time, size, num_transactions) = row?;
568
569                        histograms.block_time.push_back(time.map(|i| i as f64 / MILLIS_PER_UNIT));
570                        histograms.block_size.push_back(size.map(|i| i as u64));
571                        histograms.block_transactions.push_back(num_transactions as u64);
572                        histograms.block_heights.push_back(height as u64);
573                        Ok(histograms)
574                    },
575                )
576                .await?;
577
578            while histograms.block_time.len() > EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES {
579                histograms.block_time.pop_front();
580                histograms.block_size.pop_front();
581                histograms.block_transactions.pop_front();
582                histograms.block_heights.pop_front();
583            }
584
585            histograms
586        };
587
588        let genesis_overview = {
589            let blocks = NodeStorage::<Types>::block_height(self).await? as u64;
590            let transactions =
591                NodeStorage::<Types>::count_transactions_in_range(self, .., None).await? as u64;
592            GenesisOverview {
593                rollups: 0,
594                transactions,
595                blocks,
596            }
597        };
598
599        let latest_block: BlockDetail<Types> =
600            self.get_block_detail(BlockIdentifier::Latest).await?;
601
602        let latest_blocks: Vec<BlockSummary<Types>> = self
603            .get_block_summaries(GetBlockSummariesRequest(BlockRange {
604                target: BlockIdentifier::Latest,
605                num_blocks: NonZeroUsize::new(EXPLORER_SUMMARY_NUM_BLOCKS).unwrap(),
606            }))
607            .await?;
608
609        let latest_transactions: Vec<TransactionSummary<Types>> = self
610            .get_transaction_summaries(GetTransactionSummariesRequest {
611                range: TransactionRange {
612                    target: TransactionIdentifier::Latest,
613                    num_transactions: NonZeroUsize::new(EXPLORER_SUMMARY_NUM_TRANSACTIONS).unwrap(),
614                },
615                filter: TransactionSummaryFilter::None,
616            })
617            .await?;
618
619        Ok(ExplorerSummary {
620            genesis_overview,
621            latest_block,
622            latest_transactions,
623            latest_blocks,
624            histograms,
625        })
626    }
627
628    async fn get_search_results(
629        &mut self,
630        search_query: TaggedBase64,
631    ) -> Result<SearchResult<Types>, GetSearchResultsError> {
632        let search_tag = search_query.tag();
633        let header_tag = Commitment::<Header<Types>>::tag();
634        let tx_tag = Commitment::<HotshotTransaction<Types>>::tag();
635
636        if search_tag != header_tag && search_tag != tx_tag {
637            return Err(GetSearchResultsError::InvalidQuery(errors::BadQuery {}));
638        }
639
640        let search_query_string = search_query.to_string();
641        if search_tag == header_tag {
642            let block_query = format!(
643                "SELECT {BLOCK_COLUMNS}
644                    FROM header AS h
645                    JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
646                    WHERE h.hash = $1
647                    ORDER BY h.height DESC
648                    LIMIT 1"
649            );
650            let row = query(block_query.as_str())
651                .bind(&search_query_string)
652                .fetch_one(self.as_mut())
653                .await?;
654
655            let block = BlockSummary::from_row(&row)?;
656
657            Ok(SearchResult {
658                blocks: vec![block],
659                transactions: Vec::new(),
660            })
661        } else {
662            let transactions_query = format!(
663                "SELECT {BLOCK_COLUMNS}
664                    FROM header AS h
665                    JOIN payload AS p ON (h.payload_hash, h.ns_table) = (p.hash, p.ns_table)
666                    JOIN transactions AS t ON h.height = t.block_height
667                    WHERE t.hash = $1
668                    ORDER BY h.height DESC
669                    LIMIT 5"
670            );
671            let transactions_query_rows = query(transactions_query.as_str())
672                .bind(&search_query_string)
673                .fetch(self.as_mut());
674            let transactions_query_result: Vec<TransactionSummary<Types>> = transactions_query_rows
675                .map(|row| -> Result<Vec<TransactionSummary<Types>>, QueryError>{
676                    let block = BlockQueryData::<Types>::from_row(&row?)?;
677                    let transactions = block
678                        .enumerate()
679                        .enumerate()
680                        .filter(|(_, (_, txn))| txn.commit().to_string() == search_query_string)
681                        .map(|(offset, (_, txn))| {
682                            Ok(TransactionSummary::try_from((
683                                &block, offset, txn,
684                            ))?)
685                        })
686                        .try_collect::<TransactionSummary<Types>, Vec<TransactionSummary<Types>>, QueryError>()?;
687                    Ok(transactions)
688                })
689                .try_collect::<Vec<Vec<TransactionSummary<Types>>>>()
690                .await?
691                .into_iter()
692                .flatten()
693                .collect();
694
695            Ok(SearchResult {
696                blocks: Vec::new(),
697                transactions: transactions_query_result,
698            })
699        }
700    }
701}