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            setup_test,
412        },
413        ApiState, Error,
414    };
415
416    async fn validate(client: &Client<Error, MockBase>) {
417        let explorer_summary_response: ExplorerSummaryResponse<MockTypes> =
418            client.get("explorer-summary").send().await.unwrap();
419
420        let ExplorerSummary {
421            histograms,
422            latest_block,
423            latest_blocks,
424            latest_transactions,
425            genesis_overview,
426            ..
427        } = explorer_summary_response.explorer_summary;
428
429        let GenesisOverview {
430            blocks: num_blocks,
431            transactions: num_transactions,
432            ..
433        } = genesis_overview;
434
435        assert!(num_blocks > 0);
436        assert_eq!(histograms.block_heights.len(), min(num_blocks as usize, 50));
437        assert_eq!(histograms.block_size.len(), histograms.block_heights.len());
438        assert_eq!(histograms.block_time.len(), histograms.block_heights.len());
439        assert_eq!(
440            histograms.block_transactions.len(),
441            histograms.block_heights.len()
442        );
443
444        assert_eq!(latest_block.height, num_blocks - 1);
445        assert_eq!(latest_blocks.len(), min(num_blocks as usize, 10));
446        assert_eq!(
447            latest_transactions.len(),
448            min(num_transactions as usize, 10)
449        );
450
451        {
452            // Retrieve Block Detail using the block height
453            let block_detail_response: BlockDetailResponse<MockTypes> = client
454                .get(format!("block/{}", latest_block.height).as_str())
455                .send()
456                .await
457                .unwrap();
458            assert_eq!(block_detail_response.block_detail, latest_block);
459        }
460
461        {
462            // Retrieve Block Detail using the block hash
463            let block_detail_response: BlockDetailResponse<MockTypes> = client
464                .get(format!("block/hash/{}", latest_block.hash).as_str())
465                .send()
466                .await
467                .unwrap();
468            assert_eq!(block_detail_response.block_detail, latest_block);
469        }
470
471        {
472            // Retrieve 20 Block Summaries using the block height
473            let block_summaries_response: BlockSummaryResponse<MockTypes> = client
474                .get(format!("blocks/{}/{}", num_blocks - 1, 20).as_str())
475                .send()
476                .await
477                .unwrap();
478            for (a, b) in block_summaries_response
479                .block_summaries
480                .iter()
481                .zip(latest_blocks.iter())
482            {
483                assert_eq!(a, b);
484            }
485        }
486
487        {
488            let target_num = min(num_blocks as usize, 10);
489            // Retrieve the 20 latest block summaries
490            let block_summaries_response: BlockSummaryResponse<MockTypes> = client
491                .get(format!("blocks/latest/{target_num}").as_str())
492                .send()
493                .await
494                .unwrap();
495
496            // These blocks aren't guaranteed to have any overlap with what has
497            // been previously generated, so we don't know if we can check
498            // equality of the set.  However, we **can** check to see if the
499            // number of blocks we were asking for get returned.
500            assert_eq!(block_summaries_response.block_summaries.len(), target_num);
501
502            // We can also perform a check on the first block to ensure that it
503            // is larger than or equal to our `num_blocks` variable.
504            assert!(
505                block_summaries_response
506                    .block_summaries
507                    .first()
508                    .unwrap()
509                    .height
510                    >= num_blocks - 1
511            );
512        }
513        let get_search_response: SearchResultResponse<MockTypes> = client
514            .get(format!("search/{}", latest_block.hash).as_str())
515            .send()
516            .await
517            .unwrap();
518
519        assert!(!get_search_response.search_results.blocks.is_empty());
520
521        if num_transactions > 0 {
522            let last_transaction = latest_transactions.first().unwrap();
523            let transaction_detail_response: TransactionDetailResponse<MockTypes> = client
524                .get(format!("transaction/hash/{}", last_transaction.hash).as_str())
525                .send()
526                .await
527                .unwrap();
528
529            assert!(
530                transaction_detail_response
531                    .transaction_detail
532                    .details
533                    .block_confirmed
534            );
535
536            assert_eq!(
537                transaction_detail_response.transaction_detail.details.hash,
538                last_transaction.hash
539            );
540
541            assert_eq!(
542                transaction_detail_response
543                    .transaction_detail
544                    .details
545                    .height,
546                last_transaction.height
547            );
548
549            assert_eq!(
550                transaction_detail_response
551                    .transaction_detail
552                    .details
553                    .num_transactions,
554                last_transaction.num_transactions
555            );
556
557            assert_eq!(
558                transaction_detail_response
559                    .transaction_detail
560                    .details
561                    .offset,
562                last_transaction.offset
563            );
564            // assert_eq!(transaction_detail_response.transaction_detail.details.size, last_transaction.size);
565
566            assert_eq!(
567                transaction_detail_response.transaction_detail.details.time,
568                last_transaction.time
569            );
570
571            // Transactions Summaries - No Filter
572            let n_txns = num_txns_per_block();
573
574            {
575                // Retrieve transactions summaries via hash
576                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
577                    client
578                        .get(format!("transactions/hash/{}/{}", last_transaction.hash, 20).as_str())
579                        .send()
580                        .await
581                        .unwrap();
582
583                for (a, b) in transaction_summaries_response
584                    .transaction_summaries
585                    .iter()
586                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
587                {
588                    assert_eq!(a, b);
589                }
590            }
591
592            {
593                // Retrieve transactions summaries via height and offset
594                // No offset, which should indicate the most recent transaction
595                // within the targeted block.
596                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
597                    client
598                        .get(
599                            format!("transactions/from/{}/{}/{}", last_transaction.height, 0, 20)
600                                .as_str(),
601                        )
602                        .send()
603                        .await
604                        .unwrap();
605
606                for (a, b) in transaction_summaries_response
607                    .transaction_summaries
608                    .iter()
609                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
610                {
611                    assert_eq!(a, b);
612                }
613            }
614
615            {
616                // Retrieve transactions summaries via height and offset (different offset)
617                // In this case since we're creating n_txns transactions per
618                // block, an offset of n_txns - 1 will ensure that we're still
619                // within the same starting target block.
620                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
621                    client
622                        .get(
623                            format!(
624                                "transactions/from/{}/{}/{}",
625                                last_transaction.height,
626                                n_txns - 1,
627                                20
628                            )
629                            .as_str(),
630                        )
631                        .send()
632                        .await
633                        .unwrap();
634
635                for (a, b) in transaction_summaries_response
636                    .transaction_summaries
637                    .iter()
638                    .zip(
639                        latest_transactions
640                            .iter()
641                            .skip(n_txns - 1)
642                            .take(10)
643                            .collect::<Vec<_>>(),
644                    )
645                {
646                    assert_eq!(a, b);
647                }
648            }
649
650            {
651                // Retrieve transactions summaries via height and offset (different offset)
652                // In this case since we're creating n_txns transactions per
653                // block, an offset of n_txns + 1 will ensure that we're
654                // outside of the starting block
655                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
656                    client
657                        .get(
658                            format!(
659                                "transactions/from/{}/{}/{}",
660                                last_transaction.height,
661                                n_txns + 1,
662                                20
663                            )
664                            .as_str(),
665                        )
666                        .send()
667                        .await
668                        .unwrap();
669
670                for (a, b) in transaction_summaries_response
671                    .transaction_summaries
672                    .iter()
673                    .zip(
674                        latest_transactions
675                            .iter()
676                            .skip(6)
677                            .take(10)
678                            .collect::<Vec<_>>(),
679                    )
680                {
681                    assert_eq!(a, b);
682                }
683            }
684
685            {
686                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
687                    client
688                        .get(format!("transactions/latest/{}", 20).as_str())
689                        .send()
690                        .await
691                        .unwrap();
692
693                for (a, b) in transaction_summaries_response
694                    .transaction_summaries
695                    .iter()
696                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
697                {
698                    assert_eq!(a, b);
699                }
700            }
701
702            // Transactions Summaries - Block Filter
703
704            {
705                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
706                    client
707                        .get(
708                            format!(
709                                "transactions/hash/{}/{}/block/{}",
710                                last_transaction.hash, 20, last_transaction.height
711                            )
712                            .as_str(),
713                        )
714                        .send()
715                        .await
716                        .unwrap();
717
718                for (a, b) in transaction_summaries_response
719                    .transaction_summaries
720                    .iter()
721                    .take_while(|t: &&TransactionSummary<MockTypes>| {
722                        t.height == last_transaction.height
723                    })
724                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
725                {
726                    assert_eq!(a, b);
727                }
728            }
729
730            {
731                // With an offset of 0, we should start at the most recent
732                // transaction within the specified block.
733                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
734                    client
735                        .get(
736                            format!(
737                                "transactions/from/{}/{}/{}/block/{}",
738                                last_transaction.height, 0, 20, last_transaction.height
739                            )
740                            .as_str(),
741                        )
742                        .send()
743                        .await
744                        .unwrap();
745
746                for (a, b) in transaction_summaries_response
747                    .transaction_summaries
748                    .iter()
749                    .take_while(|t: &&TransactionSummary<MockTypes>| {
750                        t.height == last_transaction.height
751                    })
752                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
753                {
754                    assert_eq!(a, b);
755                }
756            }
757
758            {
759                // In this case, since we're creating n_txns transactions per
760                // block, an offset of n_txns - 1 will ensure that we're still
761                // within the same starting target block.
762                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
763                    client
764                        .get(
765                            format!(
766                                "transactions/from/{}/{}/{}/block/{}",
767                                last_transaction.height,
768                                n_txns - 1,
769                                20,
770                                last_transaction.height
771                            )
772                            .as_str(),
773                        )
774                        .send()
775                        .await
776                        .unwrap();
777
778                for (a, b) in transaction_summaries_response
779                    .transaction_summaries
780                    .iter()
781                    .skip(n_txns - 1)
782                    .take_while(|t: &&TransactionSummary<MockTypes>| {
783                        t.height == last_transaction.height
784                    })
785                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
786                {
787                    assert_eq!(a, b);
788                }
789            }
790
791            {
792                // In this case, since we're creating n_txns transactions per
793                // block, an offset of n_txns + 1 will ensure that we're
794                // outside of the starting target block
795                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
796                    client
797                        .get(
798                            format!(
799                                "transactions/from/{}/{}/{}/block/{}",
800                                last_transaction.height,
801                                n_txns + 1,
802                                20,
803                                last_transaction.height
804                            )
805                            .as_str(),
806                        )
807                        .send()
808                        .await
809                        .unwrap();
810
811                for (a, b) in transaction_summaries_response
812                    .transaction_summaries
813                    .iter()
814                    .skip(n_txns + 1)
815                    .take_while(|t: &&TransactionSummary<MockTypes>| {
816                        t.height == last_transaction.height
817                    })
818                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
819                {
820                    assert_eq!(a, b);
821                }
822            }
823
824            {
825                let transaction_summaries_response: TransactionSummariesResponse<MockTypes> =
826                    client
827                        .get(
828                            format!(
829                                "transactions/latest/{}/block/{}",
830                                20, last_transaction.height
831                            )
832                            .as_str(),
833                        )
834                        .send()
835                        .await
836                        .unwrap();
837
838                for (a, b) in transaction_summaries_response
839                    .transaction_summaries
840                    .iter()
841                    .take_while(|t: &&TransactionSummary<MockTypes>| {
842                        t.height == last_transaction.height
843                    })
844                    .zip(latest_transactions.iter().take(10).collect::<Vec<_>>())
845                {
846                    assert_eq!(a, b);
847                }
848            }
849        }
850    }
851
852    #[tokio::test(flavor = "multi_thread")]
853    async fn test_api() {
854        test_api_helper().await;
855    }
856
857    fn num_blocks() -> usize {
858        10
859    }
860
861    fn num_txns_per_block() -> usize {
862        5
863    }
864
865    async fn test_api_helper() {
866        setup_test();
867
868        // Create the consensus network.
869        let mut network = MockNetwork::<MockSqlDataSource, MockVersions>::init().await;
870        network.start().await;
871
872        // Start the web server.
873        let port = pick_unused_port().unwrap();
874        let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source()));
875        app.register_module(
876            "explorer",
877            define_api(MockBase::instance(), "0.0.1".parse().unwrap()).unwrap(),
878        )
879        .unwrap();
880        app.register_module(
881            "availability",
882            availability::define_api(
883                &availability::Options {
884                    fetch_timeout: Duration::from_secs(5),
885                    ..Default::default()
886                },
887                MockBase::instance(),
888                "0.0.1".parse().unwrap(),
889            )
890            .unwrap(),
891        )
892        .unwrap();
893
894        network.spawn(
895            "server",
896            app.serve(format!("0.0.0.0:{port}"), MockBase::instance()),
897        );
898
899        // Start a client.
900        let availability_client = Client::<Error, MockBase>::new(
901            format!("http://localhost:{port}/availability")
902                .parse()
903                .unwrap(),
904        );
905        let explorer_client = Client::<Error, MockBase>::new(
906            format!("http://localhost:{port}/explorer").parse().unwrap(),
907        );
908
909        assert!(
910            availability_client
911                .connect(Some(Duration::from_secs(60)))
912                .await
913        );
914
915        let mut blocks = availability_client
916            .socket("stream/blocks/0")
917            .subscribe::<availability::BlockQueryData<MockTypes>>()
918            .await
919            .unwrap();
920
921        let n_blocks = num_blocks();
922        let n_txns = num_txns_per_block();
923        for b in 0..n_blocks {
924            for t in 0..n_txns {
925                let nonce = b * n_txns + t;
926                let txn: hotshot_example_types::block_types::TestTransaction =
927                    mock_transaction(vec![nonce as u8]);
928                network.submit_transaction(txn).await;
929            }
930
931            // Wait for the transaction to be finalized.
932            for _ in 0..10 {
933                let block = blocks.next().await.unwrap();
934                let block = block.unwrap();
935
936                if !block.is_empty() {
937                    break;
938                }
939            }
940        }
941
942        assert!(explorer_client.connect(Some(Duration::from_secs(60))).await);
943
944        // sleep a little bit to give some chance for blocks to be generated.
945        validate(&explorer_client).await;
946        network.shut_down().await;
947    }
948}