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, 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#[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 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 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 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 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 let block_summaries_response: BlockSummaryResponse<MockTypes> = client
489 .get(format!("blocks/latest/{target_num}").as_str())
490 .send()
491 .await
492 .unwrap();
493
494 assert_eq!(block_summaries_response.block_summaries.len(), target_num);
499
500 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!(
565 transaction_detail_response.transaction_detail.details.time,
566 last_transaction.time
567 );
568
569 let n_txns = num_txns_per_block();
571
572 {
573 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 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 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 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 {
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 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 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 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 let mut network = MockNetwork::<MockSqlDataSource>::init().await;
866 network.start().await;
867
868 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 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 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 validate(&explorer_client).await;
942 network.shut_down().await;
943 }
944}