1pub(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#[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#[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#[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#[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#[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#[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
180pub 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 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 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 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 let block_summaries_response: BlockSummaryResponse<MockTypes> = client
433 .get(format!("blocks/latest/{target_num}").as_str())
434 .send()
435 .await
436 .unwrap();
437
438 assert_eq!(block_summaries_response.block_summaries.len(), target_num);
443
444 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!(
509 transaction_detail_response.transaction_detail.details.time,
510 last_transaction.time
511 );
512
513 let n_txns = num_txns_per_block();
515
516 {
517 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 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 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 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 {
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 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 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 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 let mut network = MockNetwork::<MockSqlDataSource>::init().await;
810 network.start().await;
811
812 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 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 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 validate(&explorer_client).await;
886 network.shut_down().await;
887 }
888}