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