1pub(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#[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#[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#[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#[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#[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#[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#[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
236pub 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 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 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 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 let block_summaries_response: BlockSummaryResponse<MockTypes> = client
491 .get(format!("blocks/latest/{target_num}").as_str())
492 .send()
493 .await
494 .unwrap();
495
496 assert_eq!(block_summaries_response.block_summaries.len(), target_num);
501
502 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!(
567 transaction_detail_response.transaction_detail.details.time,
568 last_transaction.time
569 );
570
571 let n_txns = num_txns_per_block();
573
574 {
575 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 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 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 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 {
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 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 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 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 let mut network = MockNetwork::<MockSqlDataSource, MockVersions>::init().await;
870 network.start().await;
871
872 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 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 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 validate(&explorer_client).await;
946 network.shut_down().await;
947 }
948}