hotshot_query_service/
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
13pub(crate) mod currency;
14pub(crate) mod data_source;
15pub(crate) mod errors;
16pub(crate) mod monetary_value;
17pub(crate) mod query_data;
18pub(crate) mod traits;
19
20use std::{fmt::Display, num::NonZeroUsize, path::Path};
21
22pub use currency::*;
23pub use data_source::*;
24use futures::FutureExt;
25use hotshot_types::traits::node_implementation::NodeType;
26pub use monetary_value::*;
27pub use query_data::*;
28use serde::{Deserialize, Serialize};
29use tide_disco::{api::ApiError, method::ReadState, Api, StatusCode};
30pub use traits::*;
31use vbs::version::StaticVersionType;
32
33use self::errors::InvalidLimit;
34use crate::{
35    api::load_api,
36    availability::{QueryableHeader, QueryablePayload},
37    Header, Payload, Transaction,
38};
39
40/// [Error] is an enum that represents the various errors that can be returned
41/// from the Explorer API.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(untagged)]
44pub enum Error {
45    GetBlockDetail(GetBlockDetailError),
46    GetBlockSummaries(GetBlockSummariesError),
47    GetTransactionDetail(GetTransactionDetailError),
48    GetTransactionSummaries(GetTransactionSummariesError),
49    GetExplorerSummary(GetExplorerSummaryError),
50    GetSearchResults(GetSearchResultsError),
51}
52
53impl Error {
54    pub fn status(&self) -> StatusCode {
55        match self {
56            Error::GetBlockDetail(e) => e.status(),
57            Error::GetBlockSummaries(e) => e.status(),
58            Error::GetTransactionDetail(e) => e.status(),
59            Error::GetTransactionSummaries(e) => e.status(),
60            Error::GetExplorerSummary(e) => e.status(),
61            Error::GetSearchResults(e) => e.status(),
62        }
63    }
64}
65
66impl Display for Error {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Error::GetBlockDetail(e) => e.fmt(f),
70            Error::GetBlockSummaries(e) => e.fmt(f),
71            Error::GetTransactionDetail(e) => e.fmt(f),
72            Error::GetTransactionSummaries(e) => e.fmt(f),
73            Error::GetExplorerSummary(e) => e.fmt(f),
74            Error::GetSearchResults(e) => e.fmt(f),
75        }
76    }
77}
78
79impl std::error::Error for Error {
80    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81        match self {
82            Error::GetBlockDetail(e) => Some(e),
83            Error::GetBlockSummaries(e) => Some(e),
84            Error::GetTransactionDetail(e) => Some(e),
85            Error::GetTransactionSummaries(e) => Some(e),
86            Error::GetExplorerSummary(e) => Some(e),
87            Error::GetSearchResults(e) => Some(e),
88        }
89    }
90}
91
92/// [BlockDetailResponse] is a struct that represents the response from the
93/// `get_block_detail` endpoint.
94#[derive(Debug, Serialize, Deserialize)]
95#[serde(bound = "")]
96pub struct BlockDetailResponse<Types: NodeType>
97where
98    Header<Types>: ExplorerHeader<Types>,
99{
100    pub block_detail: BlockDetail<Types>,
101}
102
103impl<Types: NodeType> From<BlockDetail<Types>> for BlockDetailResponse<Types>
104where
105    Header<Types>: ExplorerHeader<Types>,
106{
107    fn from(block_detail: BlockDetail<Types>) -> Self {
108        Self { block_detail }
109    }
110}
111
112/// [BlockSummaryResponse] is a struct that represents the response from the
113/// `get_block_summaries` endpoint.
114#[derive(Debug, Serialize, Deserialize)]
115#[serde(bound = "")]
116pub struct BlockSummaryResponse<Types: NodeType>
117where
118    Header<Types>: ExplorerHeader<Types>,
119{
120    pub block_summaries: Vec<BlockSummary<Types>>,
121}
122
123impl<Types: NodeType> From<Vec<BlockSummary<Types>>> for BlockSummaryResponse<Types>
124where
125    Header<Types>: ExplorerHeader<Types>,
126{
127    fn from(block_summaries: Vec<BlockSummary<Types>>) -> Self {
128        Self { block_summaries }
129    }
130}
131
132/// [TransactionDetailResponse] is a struct that represents the response from the
133/// `get_transaction_detail` endpoint.
134#[derive(Debug, Serialize, Deserialize)]
135#[serde(bound = "")]
136pub struct TransactionDetailResponse<Types: NodeType> {
137    pub transaction_detail: query_data::TransactionDetailResponse<Types>,
138}
139
140impl<Types: NodeType> From<query_data::TransactionDetailResponse<Types>>
141    for TransactionDetailResponse<Types>
142{
143    fn from(transaction_detail: query_data::TransactionDetailResponse<Types>) -> Self {
144        Self { transaction_detail }
145    }
146}
147
148/// [TransactionSummariesResponse] is a struct that represents the response from the
149/// `get_transaction_summaries` endpoint.
150#[derive(Debug, Serialize, Deserialize)]
151#[serde(bound = "")]
152pub struct TransactionSummariesResponse<Types: NodeType>
153where
154    Header<Types>: ExplorerHeader<Types>,
155    Transaction<Types>: ExplorerTransaction<Types>,
156{
157    pub transaction_summaries: Vec<TransactionSummary<Types>>,
158}
159
160impl<Types: NodeType> From<Vec<TransactionSummary<Types>>> for TransactionSummariesResponse<Types>
161where
162    Header<Types>: ExplorerHeader<Types>,
163    Transaction<Types>: ExplorerTransaction<Types>,
164{
165    fn from(transaction_summaries: Vec<TransactionSummary<Types>>) -> Self {
166        Self {
167            transaction_summaries,
168        }
169    }
170}
171
172/// [ExplorerSummaryResponse] is a struct that represents the response from the
173/// `get_explorer_summary` endpoint.
174#[derive(Debug, Serialize, Deserialize)]
175#[serde(bound = "")]
176pub struct ExplorerSummaryResponse<Types: NodeType>
177where
178    Header<Types>: ExplorerHeader<Types>,
179    Transaction<Types>: ExplorerTransaction<Types>,
180{
181    pub explorer_summary: ExplorerSummary<Types>,
182}
183
184impl<Types: NodeType> From<ExplorerSummary<Types>> for ExplorerSummaryResponse<Types>
185where
186    Header<Types>: ExplorerHeader<Types>,
187    Transaction<Types>: ExplorerTransaction<Types>,
188{
189    fn from(explorer_summary: ExplorerSummary<Types>) -> Self {
190        Self { explorer_summary }
191    }
192}
193
194/// [SearchResultResponse] is a struct that represents the response from the
195/// `get_search_result` endpoint.
196#[derive(Debug, Serialize, Deserialize)]
197#[serde(bound = "")]
198pub struct SearchResultResponse<Types: NodeType>
199where
200    Header<Types>: ExplorerHeader<Types>,
201    Transaction<Types>: ExplorerTransaction<Types>,
202{
203    pub search_results: SearchResult<Types>,
204}
205
206impl<Types: NodeType> From<SearchResult<Types>> for SearchResultResponse<Types>
207where
208    Header<Types>: ExplorerHeader<Types>,
209    Transaction<Types>: ExplorerTransaction<Types>,
210{
211    fn from(search_results: SearchResult<Types>) -> Self {
212        Self { search_results }
213    }
214}
215
216fn validate_limit(
217    limit: Result<usize, tide_disco::RequestError>,
218) -> Result<NonZeroUsize, InvalidLimit> {
219    let num_blocks = match limit {
220        Ok(limit) => Ok(limit),
221        _ => Err(InvalidLimit {}),
222    }?;
223
224    let num_blocks = match NonZeroUsize::new(num_blocks) {
225        Some(num_blocks) => Ok(num_blocks),
226        None => Err(InvalidLimit {}),
227    }?;
228
229    if num_blocks.get() > 100 {
230        return Err(InvalidLimit {});
231    }
232
233    Ok(num_blocks)
234}
235
236/// `define_api` is a function that defines the API endpoints for the Explorer
237/// module of the HotShot Query Service. It implements the specification
238/// defined in the `explorer.toml` file.
239pub fn define_api<State, Types: NodeType, Ver: StaticVersionType + 'static>(
240    _: Ver,
241    api_ver: semver::Version,
242) -> Result<Api<State, Error, Ver>, ApiError>
243where
244    State: 'static + Send + Sync + ReadState,
245    Header<Types>: ExplorerHeader<Types> + QueryableHeader<Types>,
246    Transaction<Types>: ExplorerTransaction<Types>,
247    Payload<Types>: QueryablePayload<Types>,
248    <State as ReadState>::State: ExplorerDataSource<Types> + Send + Sync,
249{
250    let mut api = load_api::<State, Error, Ver>(
251        Option::<Box<Path>>::None,
252        include_str!("../api/explorer.toml"),
253        None,
254    )?;
255
256    api.with_version(api_ver)
257        .get("get_block_detail", move |req, state| {
258            async move {
259                let target = match (
260                    req.opt_integer_param::<str, usize>("height"),
261                    req.opt_blob_param("hash"),
262                ) {
263                    (Ok(Some(from)), _) => BlockIdentifier::Height(from),
264                    (_, Ok(Some(hash))) => BlockIdentifier::Hash(hash),
265                    _ => BlockIdentifier::Latest,
266                };
267
268                state
269                    .get_block_detail(target)
270                    .await
271                    .map(BlockDetailResponse::from)
272                    .map_err(Error::GetBlockDetail)
273            }
274            .boxed()
275        })?
276        .get("get_block_summaries", move |req, state| {
277            async move {
278                let num_blocks = validate_limit(req.integer_param("limit"))
279                    .map_err(GetBlockSummariesError::InvalidLimit)
280                    .map_err(Error::GetBlockSummaries)?;
281
282                let target = match (
283                    req.opt_integer_param::<str, usize>("from"),
284                    req.opt_blob_param("hash"),
285                ) {
286                    (Ok(Some(from)), _) => BlockIdentifier::Height(from),
287                    (_, Ok(Some(hash))) => BlockIdentifier::Hash(hash),
288                    _ => BlockIdentifier::Latest,
289                };
290
291                state
292                    .get_block_summaries(GetBlockSummariesRequest(BlockRange {
293                        target,
294                        num_blocks,
295                    }))
296                    .await
297                    .map(BlockSummaryResponse::from)
298                    .map_err(Error::GetBlockSummaries)
299            }
300            .boxed()
301        })?
302        .get("get_transaction_detail", move |req, state| {
303            async move {
304                state
305                    .get_transaction_detail(
306                        match (
307                            req.opt_integer_param("height"),
308                            req.opt_integer_param("offset"),
309                            req.opt_blob_param("hash"),
310                        ) {
311                            (Ok(Some(height)), Ok(Some(offset)), _) => {
312                                TransactionIdentifier::HeightAndOffset(height, offset)
313                            },
314                            (_, _, Ok(Some(hash))) => TransactionIdentifier::Hash(hash),
315                            _ => TransactionIdentifier::Latest,
316                        },
317                    )
318                    .await
319                    .map(TransactionDetailResponse::from)
320                    .map_err(Error::GetTransactionDetail)
321            }
322            .boxed()
323        })?
324        .get("get_transaction_summaries", move |req, state| {
325            async move {
326                let num_transactions = validate_limit(req.integer_param("limit"))
327                    .map_err(GetTransactionSummariesError::InvalidLimit)
328                    .map_err(Error::GetTransactionSummaries)?;
329
330                let filter = match (
331                    req.opt_integer_param("block"),
332                    req.opt_integer_param::<_, i64>("namespace"),
333                ) {
334                    (Ok(Some(block)), _) => TransactionSummaryFilter::Block(block),
335                    (_, Ok(Some(namespace))) => TransactionSummaryFilter::RollUp(namespace.into()),
336                    _ => TransactionSummaryFilter::None,
337                };
338
339                let target = match (
340                    req.opt_integer_param::<str, usize>("height"),
341                    req.opt_integer_param::<str, usize>("offset"),
342                    req.opt_blob_param("hash"),
343                ) {
344                    (Ok(Some(height)), Ok(Some(offset)), _) => {
345                        TransactionIdentifier::HeightAndOffset(height, offset)
346                    },
347                    (_, _, Ok(Some(hash))) => TransactionIdentifier::Hash(hash),
348                    _ => TransactionIdentifier::Latest,
349                };
350
351                state
352                    .get_transaction_summaries(GetTransactionSummariesRequest {
353                        range: TransactionRange {
354                            target,
355                            num_transactions,
356                        },
357                        filter,
358                    })
359                    .await
360                    .map(TransactionSummariesResponse::from)
361                    .map_err(Error::GetTransactionSummaries)
362            }
363            .boxed()
364        })?
365        .get("get_explorer_summary", move |_req, state| {
366            async move {
367                state
368                    .get_explorer_summary()
369                    .await
370                    .map(ExplorerSummaryResponse::from)
371                    .map_err(Error::GetExplorerSummary)
372            }
373            .boxed()
374        })?
375        .get("get_search_result", move |req, state| {
376            async move {
377                let query = req
378                    .tagged_base64_param("query")
379                    .map_err(|err| {
380                        tracing::error!("query param error: {}", err);
381                        GetSearchResultsError::InvalidQuery(errors::BadQuery {})
382                    })
383                    .map_err(Error::GetSearchResults)?;
384
385                state
386                    .get_search_results(query.clone())
387                    .await
388                    .map(SearchResultResponse::from)
389                    .map_err(Error::GetSearchResults)
390            }
391            .boxed()
392        })?;
393    Ok(api)
394}
395
396#[cfg(test)]
397mod test {
398    use std::{cmp::min, time::Duration};
399
400    use futures::StreamExt;
401    use portpicker::pick_unused_port;
402    use surf_disco::Client;
403    use tide_disco::App;
404
405    use super::*;
406    use crate::{
407        availability,
408        testing::{
409            consensus::{MockNetwork, MockSqlDataSource},
410            mocks::{mock_transaction, MockBase, MockTypes, MockVersions},
411        },
412        ApiState, Error,
413    };
414
415    async fn validate(client: &Client<Error, MockBase>) {
416        let explorer_summary_response: ExplorerSummaryResponse<MockTypes> =
417            client.get("explorer-summary").send().await.unwrap();
418
419        let ExplorerSummary {
420            histograms,
421            latest_block,
422            latest_blocks,
423            latest_transactions,
424            genesis_overview,
425            ..
426        } = explorer_summary_response.explorer_summary;
427
428        let GenesisOverview {
429            blocks: num_blocks,
430            transactions: num_transactions,
431            ..
432        } = genesis_overview;
433
434        assert!(num_blocks > 0);
435        assert_eq!(histograms.block_heights.len(), min(num_blocks as usize, 50));
436        assert_eq!(histograms.block_size.len(), histograms.block_heights.len());
437        assert_eq!(histograms.block_time.len(), histograms.block_heights.len());
438        assert_eq!(
439            histograms.block_transactions.len(),
440            histograms.block_heights.len()
441        );
442
443        assert_eq!(latest_block.height, num_blocks - 1);
444        assert_eq!(latest_blocks.len(), min(num_blocks as usize, 10));
445        assert_eq!(
446            latest_transactions.len(),
447            min(num_transactions as usize, 10)
448        );
449
450        {
451            // Retrieve Block Detail using the block height
452            let block_detail_response: BlockDetailResponse<MockTypes> = client
453                .get(format!("block/{}", latest_block.height).as_str())
454                .send()
455                .await
456                .unwrap();
457            assert_eq!(block_detail_response.block_detail, latest_block);
458        }
459
460        {
461            // Retrieve Block Detail using the block hash
462            let block_detail_response: BlockDetailResponse<MockTypes> = client
463                .get(format!("block/hash/{}", latest_block.hash).as_str())
464                .send()
465                .await
466                .unwrap();
467            assert_eq!(block_detail_response.block_detail, latest_block);
468        }
469
470        {
471            // Retrieve 20 Block Summaries using the block height
472            let block_summaries_response: BlockSummaryResponse<MockTypes> = client
473                .get(format!("blocks/{}/{}", num_blocks - 1, 20).as_str())
474                .send()
475                .await
476                .unwrap();
477            for (a, b) in block_summaries_response
478                .block_summaries
479                .iter()
480                .zip(latest_blocks.iter())
481            {
482                assert_eq!(a, b);
483            }
484        }
485
486        {
487            let target_num = min(num_blocks as usize, 10);
488            // Retrieve the 20 latest block summaries
489            let block_summaries_response: BlockSummaryResponse<MockTypes> = client
490                .get(format!("blocks/latest/{target_num}").as_str())
491                .send()
492                .await
493                .unwrap();
494
495            // These blocks aren't guaranteed to have any overlap with what has
496            // been previously generated, so we don't know if we can check
497            // equality of the set.  However, we **can** check to see if the
498            // number of blocks we were asking for get returned.
499            assert_eq!(block_summaries_response.block_summaries.len(), target_num);
500
501            // We can also perform a check on the first block to ensure that it
502            // is larger than or equal to our `num_blocks` variable.
503            assert!(
504                block_summaries_response
505                    .block_summaries
506                    .first()
507                    .unwrap()
508                    .height
509                    >= num_blocks - 1
510            );
511        }
512        let get_search_response: SearchResultResponse<MockTypes> = client
513            .get(format!("search/{}", latest_block.hash).as_str())
514            .send()
515            .await
516            .unwrap();
517
518        assert!(!get_search_response.search_results.blocks.is_empty());
519
520        if num_transactions > 0 {
521            let last_transaction = latest_transactions.first().unwrap();
522            let transaction_detail_response: TransactionDetailResponse<MockTypes> = client
523                .get(format!("transaction/hash/{}", last_transaction.hash).as_str())
524                .send()
525                .await
526                .unwrap();
527
528            assert!(
529                transaction_detail_response
530                    .transaction_detail
531                    .details
532                    .block_confirmed
533            );
534
535            assert_eq!(
536                transaction_detail_response.transaction_detail.details.hash,
537                last_transaction.hash
538            );
539
540            assert_eq!(
541                transaction_detail_response
542                    .transaction_detail
543                    .details
544                    .height,
545                last_transaction.height
546            );
547
548            assert_eq!(
549                transaction_detail_response
550                    .transaction_detail
551                    .details
552                    .num_transactions,
553                last_transaction.num_transactions
554            );
555
556            assert_eq!(
557                transaction_detail_response
558                    .transaction_detail
559                    .details
560                    .offset,
561                last_transaction.offset
562            );
563            // assert_eq!(transaction_detail_response.transaction_detail.details.size, last_transaction.size);
564
565            assert_eq!(
566                transaction_detail_response.transaction_detail.details.time,
567                last_transaction.time
568            );
569
570            // Transactions Summaries - No Filter
571            let n_txns = num_txns_per_block();
572
573            {
574                // Retrieve transactions summaries via hash
575                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
576                    client
577                        .get(format!("transactions/hash/{}/{}", last_transaction.hash, 20).as_str())
578                        .send()
579                        .await
580                        .unwrap();
581
582                for (a, b) in transaction_summaries_response
583                    .transaction_summaries
584                    .iter()
585                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
586                {
587                    assert_eq!(a, b);
588                }
589            }
590
591            {
592                // Retrieve transactions summaries via height and offset
593                // No offset, which should indicate the most recent transaction
594                // within the targeted block.
595                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
596                    client
597                        .get(
598                            format!("transactions/from/{}/{}/{}", last_transaction.height, 0, 20)
599                                .as_str(),
600                        )
601                        .send()
602                        .await
603                        .unwrap();
604
605                for (a, b) in transaction_summaries_response
606                    .transaction_summaries
607                    .iter()
608                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
609                {
610                    assert_eq!(a, b);
611                }
612            }
613
614            {
615                // Retrieve transactions summaries via height and offset (different offset)
616                // In this case since we're creating n_txns transactions per
617                // block, an offset of n_txns - 1 will ensure that we're still
618                // within the same starting target block.
619                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
620                    client
621                        .get(
622                            format!(
623                                "transactions/from/{}/{}/{}",
624                                last_transaction.height,
625                                n_txns - 1,
626                                20
627                            )
628                            .as_str(),
629                        )
630                        .send()
631                        .await
632                        .unwrap();
633
634                for (a, b) in transaction_summaries_response
635                    .transaction_summaries
636                    .iter()
637                    .zip(
638                        latest_transactions
639                            .iter()
640                            .skip(n_txns - 1)
641                            .take(10)
642                            .collect::<Vec<_>>(),
643                    )
644                {
645                    assert_eq!(a, b);
646                }
647            }
648
649            {
650                // Retrieve transactions summaries via height and offset (different offset)
651                // In this case since we're creating n_txns transactions per
652                // block, an offset of n_txns + 1 will ensure that we're
653                // outside of the starting block
654                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
655                    client
656                        .get(
657                            format!(
658                                "transactions/from/{}/{}/{}",
659                                last_transaction.height,
660                                n_txns + 1,
661                                20
662                            )
663                            .as_str(),
664                        )
665                        .send()
666                        .await
667                        .unwrap();
668
669                for (a, b) in transaction_summaries_response
670                    .transaction_summaries
671                    .iter()
672                    .zip(
673                        latest_transactions
674                            .iter()
675                            .skip(6)
676                            .take(10)
677                            .collect::<Vec<_>>(),
678                    )
679                {
680                    assert_eq!(a, b);
681                }
682            }
683
684            {
685                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
686                    client
687                        .get(format!("transactions/latest/{}", 20).as_str())
688                        .send()
689                        .await
690                        .unwrap();
691
692                for (a, b) in transaction_summaries_response
693                    .transaction_summaries
694                    .iter()
695                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
696                {
697                    assert_eq!(a, b);
698                }
699            }
700
701            // Transactions Summaries - Block Filter
702
703            {
704                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
705                    client
706                        .get(
707                            format!(
708                                "transactions/hash/{}/{}/block/{}",
709                                last_transaction.hash, 20, last_transaction.height
710                            )
711                            .as_str(),
712                        )
713                        .send()
714                        .await
715                        .unwrap();
716
717                for (a, b) in transaction_summaries_response
718                    .transaction_summaries
719                    .iter()
720                    .take_while(|t: &&TransactionSummary<MockTypes>| {
721                        t.height == last_transaction.height
722                    })
723                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
724                {
725                    assert_eq!(a, b);
726                }
727            }
728
729            {
730                // With an offset of 0, we should start at the most recent
731                // transaction within the specified block.
732                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
733                    client
734                        .get(
735                            format!(
736                                "transactions/from/{}/{}/{}/block/{}",
737                                last_transaction.height, 0, 20, last_transaction.height
738                            )
739                            .as_str(),
740                        )
741                        .send()
742                        .await
743                        .unwrap();
744
745                for (a, b) in transaction_summaries_response
746                    .transaction_summaries
747                    .iter()
748                    .take_while(|t: &&TransactionSummary<MockTypes>| {
749                        t.height == last_transaction.height
750                    })
751                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
752                {
753                    assert_eq!(a, b);
754                }
755            }
756
757            {
758                // In this case, since we're creating n_txns transactions per
759                // block, an offset of n_txns - 1 will ensure that we're still
760                // within the same starting target block.
761                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
762                    client
763                        .get(
764                            format!(
765                                "transactions/from/{}/{}/{}/block/{}",
766                                last_transaction.height,
767                                n_txns - 1,
768                                20,
769                                last_transaction.height
770                            )
771                            .as_str(),
772                        )
773                        .send()
774                        .await
775                        .unwrap();
776
777                for (a, b) in transaction_summaries_response
778                    .transaction_summaries
779                    .iter()
780                    .skip(n_txns - 1)
781                    .take_while(|t: &&TransactionSummary<MockTypes>| {
782                        t.height == last_transaction.height
783                    })
784                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
785                {
786                    assert_eq!(a, b);
787                }
788            }
789
790            {
791                // In this case, since we're creating n_txns transactions per
792                // block, an offset of n_txns + 1 will ensure that we're
793                // outside of the starting target block
794                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
795                    client
796                        .get(
797                            format!(
798                                "transactions/from/{}/{}/{}/block/{}",
799                                last_transaction.height,
800                                n_txns + 1,
801                                20,
802                                last_transaction.height
803                            )
804                            .as_str(),
805                        )
806                        .send()
807                        .await
808                        .unwrap();
809
810                for (a, b) in transaction_summaries_response
811                    .transaction_summaries
812                    .iter()
813                    .skip(n_txns + 1)
814                    .take_while(|t: &&TransactionSummary<MockTypes>| {
815                        t.height == last_transaction.height
816                    })
817                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
818                {
819                    assert_eq!(a, b);
820                }
821            }
822
823            {
824                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
825                    client
826                        .get(
827                            format!(
828                                "transactions/latest/{}/block/{}",
829                                20, last_transaction.height
830                            )
831                            .as_str(),
832                        )
833                        .send()
834                        .await
835                        .unwrap();
836
837                for (a, b) in transaction_summaries_response
838                    .transaction_summaries
839                    .iter()
840                    .take_while(|t: &&TransactionSummary<MockTypes>| {
841                        t.height == last_transaction.height
842                    })
843                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
844                {
845                    assert_eq!(a, b);
846                }
847            }
848        }
849    }
850
851    #[test_log::test(tokio::test(flavor = "multi_thread"))]
852    async fn test_api() {
853        test_api_helper().await;
854    }
855
856    fn num_blocks() -> usize {
857        10
858    }
859
860    fn num_txns_per_block() -> usize {
861        5
862    }
863
864    async fn test_api_helper() {
865        // Create the consensus network.
866        let mut network = MockNetwork::<MockSqlDataSource, MockVersions>::init().await;
867        network.start().await;
868
869        // Start the web server.
870        let port = pick_unused_port().unwrap();
871        let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source()));
872        app.register_module(
873            "explorer",
874            define_api(MockBase::instance(), "0.0.1".parse().unwrap()).unwrap(),
875        )
876        .unwrap();
877        app.register_module(
878            "availability",
879            availability::define_api(
880                &availability::Options {
881                    fetch_timeout: Duration::from_secs(5),
882                    ..Default::default()
883                },
884                MockBase::instance(),
885                "0.0.1".parse().unwrap(),
886            )
887            .unwrap(),
888        )
889        .unwrap();
890
891        network.spawn(
892            "server",
893            app.serve(format!("0.0.0.0:{port}"), MockBase::instance()),
894        );
895
896        // Start a client.
897        let availability_client = Client::<Error, MockBase>::new(
898            format!("http://localhost:{port}/availability")
899                .parse()
900                .unwrap(),
901        );
902        let explorer_client = Client::<Error, MockBase>::new(
903            format!("http://localhost:{port}/explorer").parse().unwrap(),
904        );
905
906        assert!(
907            availability_client
908                .connect(Some(Duration::from_secs(60)))
909                .await
910        );
911
912        let mut blocks = availability_client
913            .socket("stream/blocks/0")
914            .subscribe::<availability::BlockQueryData<MockTypes>>()
915            .await
916            .unwrap();
917
918        let n_blocks = num_blocks();
919        let n_txns = num_txns_per_block();
920        for b in 0..n_blocks {
921            for t in 0..n_txns {
922                let nonce = b * n_txns + t;
923                let txn: hotshot_example_types::block_types::TestTransaction =
924                    mock_transaction(vec![nonce as u8]);
925                network.submit_transaction(txn).await;
926            }
927
928            // Wait for the transaction to be finalized.
929            for _ in 0..10 {
930                let block = blocks.next().await.unwrap();
931                let block = block.unwrap();
932
933                if !block.is_empty() {
934                    break;
935                }
936            }
937        }
938
939        assert!(explorer_client.connect(Some(Duration::from_secs(60))).await);
940
941        // sleep a little bit to give some chance for blocks to be generated.
942        validate(&explorer_client).await;
943        network.shut_down().await;
944    }
945}