1use 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 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
272const EXPLORER_SUMMARY_HISTOGRAM_NUM_ENTRIES: usize = 50;
275
276const EXPLORER_SUMMARY_NUM_BLOCKS: usize = 10;
279
280const EXPLORER_SUMMARY_NUM_TRANSACTIONS: usize = 10;
283
284const 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 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 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 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}