Skip to main content

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