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