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