Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit be1b9cb

Browse files
AlexandcoatsAlex6323grtlr
authored
feat(api): add blocks by milestone endpoints (#876)
* Add blocks by milestone endpoints * Docs * Fix build * fix query * Fix requests (#922) * Fix requests * Stupid me * Fixes the returned cursor in `feat/api/blocks-by-milestone` (#923) * limit +1 * Do it like in the other places Co-authored-by: /alex/ <[email protected]> Co-authored-by: Jochen Görtler <[email protected]> Co-authored-by: Jochen Görtler <[email protected]>
1 parent ec9f1c0 commit be1b9cb

File tree

5 files changed

+331
-7
lines changed

5 files changed

+331
-7
lines changed

documentation/api/api-explorer.yml

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,62 @@ paths:
105105
$ref: "#/components/responses/NoResults"
106106
"500":
107107
$ref: "#/components/responses/InternalError"
108+
/api/explorer/v2/milestones/{milestoneId}/blocks:
109+
get:
110+
tags:
111+
- blocks
112+
summary: Returns blocks in a given milestone by ID.
113+
description: >-
114+
Returns block IDs in a given milestone by ID sorted by white flag index.
115+
parameters:
116+
- $ref: "#/components/parameters/milestoneId"
117+
- $ref: "#/components/parameters/sort"
118+
- $ref: "#/components/parameters/pageSize"
119+
- $ref: "#/components/parameters/cursor"
120+
responses:
121+
"200":
122+
description: Successful operation.
123+
content:
124+
application/json:
125+
schema:
126+
$ref: "#/components/schemas/BlocksByMilestoneResponse"
127+
examples:
128+
default:
129+
$ref: "#/components/examples/blocks-by-milestone-example"
130+
"400":
131+
$ref: "#/components/responses/BadRequest"
132+
"404":
133+
$ref: "#/components/responses/NoResults"
134+
"500":
135+
$ref: "#/components/responses/InternalError"
136+
/api/explorer/v2/milestones/by-index/{milestoneIndex}/blocks:
137+
get:
138+
tags:
139+
- blocks
140+
summary: Returns blocks in a given milestone by index.
141+
description: >-
142+
Returns block IDs in a given milestone by index sorted by white flag index.
143+
parameters:
144+
- $ref: "#/components/parameters/milestoneIndex"
145+
- $ref: "#/components/parameters/sort"
146+
- $ref: "#/components/parameters/pageSize"
147+
- $ref: "#/components/parameters/cursor"
148+
responses:
149+
"200":
150+
description: Successful operation.
151+
content:
152+
application/json:
153+
schema:
154+
$ref: "#/components/schemas/BlocksByMilestoneResponse"
155+
examples:
156+
default:
157+
$ref: "#/components/examples/blocks-by-milestone-example"
158+
"400":
159+
$ref: "#/components/responses/BadRequest"
160+
"404":
161+
$ref: "#/components/responses/NoResults"
162+
"500":
163+
$ref: "#/components/responses/InternalError"
108164
/api/explorer/v2/ledger/updates/by-address/{address}:
109165
get:
110166
tags:
@@ -336,6 +392,19 @@ components:
336392
description: The cursor which can be used to retrieve the next logical page of results.
337393
required:
338394
- items
395+
BlocksByMilestoneResponse:
396+
description: Paged block IDs by milestone.
397+
properties:
398+
blocks:
399+
type: array
400+
description: A list of block ids.
401+
items:
402+
type: string
403+
cursor:
404+
type: string
405+
description: The cursor which can be used to retrieve the next logical page of results.
406+
required:
407+
- blocks
339408
RichestAddressesResponse:
340409
description: Richest addresses statistics.
341410
properties:
@@ -486,6 +555,15 @@ components:
486555
description: >-
487556
The milestone index to be used to determine the ledger state. Defaults to the
488557
application's current ledger index.
558+
milestoneIndex:
559+
in: path
560+
name: milestoneIndex
561+
schema:
562+
type: integer
563+
example: 200000
564+
required: true
565+
description: >-
566+
The milestone index to be used.
489567
top:
490568
in: query
491569
name: top
@@ -531,7 +609,7 @@ components:
531609
index: 100
532610
- milestoneId: "0xfa0de75d225cca2799395e5fc340702fc7eac821d2bdd79911126f131ae097a2"
533611
index: 101
534-
cursor: 102.2
612+
cursor: "102.2"
535613
richest-addresses-example:
536614
value:
537615
top:
@@ -563,3 +641,10 @@ components:
563641
addressCount: "27"
564642
totalBalance: "25486528000"
565643
ledgerIndex: 1005429
644+
blocks-by-milestone-example:
645+
value:
646+
blocks:
647+
- "0xd0d361341fa3bb2f6855039a82ee9ea470c3336eaf34d22767fdfa901ba63e31"
648+
- "0x7a09324557e9200f39bf493fc8fd6ac43e9ca750c6f6d884cc72386ddcb7d695"
649+
- "0xfa0de75d225cca2799395e5fc340702fc7eac821d2bdd79911126f131ae097a2"
650+
cursor: "4.100"

src/bin/inx-chronicle/api/stardust/explorer/extractors.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,123 @@ impl<B: Send> FromRequest<B> for MilestoneRange {
330330
}
331331
}
332332

333+
pub struct BlocksByMilestoneIndexPagination {
334+
pub sort: SortOrder,
335+
pub page_size: usize,
336+
pub cursor: Option<u32>,
337+
}
338+
339+
#[derive(Clone, Deserialize, Default)]
340+
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
341+
pub struct BlocksByMilestoneIndexPaginationQuery {
342+
pub sort: Option<String>,
343+
pub page_size: Option<usize>,
344+
pub cursor: Option<String>,
345+
}
346+
347+
#[derive(Clone)]
348+
pub struct BlocksByMilestoneCursor {
349+
pub white_flag_index: u32,
350+
pub page_size: usize,
351+
}
352+
353+
impl FromStr for BlocksByMilestoneCursor {
354+
type Err = ApiError;
355+
356+
fn from_str(s: &str) -> Result<Self, Self::Err> {
357+
let parts: Vec<_> = s.split('.').collect();
358+
Ok(match parts[..] {
359+
[wfi, ps] => BlocksByMilestoneCursor {
360+
white_flag_index: wfi.parse().map_err(RequestError::from)?,
361+
page_size: ps.parse().map_err(RequestError::from)?,
362+
},
363+
_ => return Err(ApiError::from(RequestError::BadPagingState)),
364+
})
365+
}
366+
}
367+
368+
impl Display for BlocksByMilestoneCursor {
369+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370+
write!(f, "{}.{}", self.white_flag_index, self.page_size)
371+
}
372+
}
373+
374+
#[async_trait]
375+
impl<B: Send> FromRequest<B> for BlocksByMilestoneIndexPagination {
376+
type Rejection = ApiError;
377+
378+
async fn from_request(req: &mut axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
379+
let Query(query) = Query::<BlocksByMilestoneIndexPaginationQuery>::from_request(req)
380+
.await
381+
.map_err(RequestError::from)?;
382+
let Extension(config) = Extension::<ApiData>::from_request(req).await?;
383+
384+
let sort = query
385+
.sort
386+
.as_deref()
387+
.map_or(Ok(Default::default()), str::parse)
388+
.map_err(RequestError::SortOrder)?;
389+
390+
let (page_size, cursor) = if let Some(cursor) = query.cursor {
391+
let cursor: BlocksByMilestoneCursor = cursor.parse()?;
392+
(cursor.page_size, Some(cursor.white_flag_index))
393+
} else {
394+
(query.page_size.unwrap_or(DEFAULT_PAGE_SIZE), None)
395+
};
396+
397+
Ok(BlocksByMilestoneIndexPagination {
398+
sort,
399+
page_size: page_size.min(config.max_page_size),
400+
cursor,
401+
})
402+
}
403+
}
404+
405+
pub struct BlocksByMilestoneIdPagination {
406+
pub sort: SortOrder,
407+
pub page_size: usize,
408+
pub cursor: Option<u32>,
409+
}
410+
411+
#[derive(Clone, Deserialize, Default)]
412+
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
413+
pub struct BlocksByMilestoneIdPaginationQuery {
414+
pub sort: Option<String>,
415+
pub page_size: Option<usize>,
416+
pub cursor: Option<String>,
417+
}
418+
419+
#[async_trait]
420+
impl<B: Send> FromRequest<B> for BlocksByMilestoneIdPagination {
421+
type Rejection = ApiError;
422+
423+
async fn from_request(req: &mut axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
424+
let Query(query) = Query::<BlocksByMilestoneIdPaginationQuery>::from_request(req)
425+
.await
426+
.map_err(RequestError::from)?;
427+
let Extension(config) = Extension::<ApiData>::from_request(req).await?;
428+
429+
let sort = query
430+
.sort
431+
.as_deref()
432+
.map_or(Ok(Default::default()), str::parse)
433+
.map_err(RequestError::SortOrder)?;
434+
435+
let (page_size, cursor) = if let Some(cursor) = query.cursor {
436+
let cursor: BlocksByMilestoneCursor = cursor.parse()?;
437+
(cursor.page_size, Some(cursor.white_flag_index))
438+
} else {
439+
(query.page_size.unwrap_or(DEFAULT_PAGE_SIZE), None)
440+
};
441+
442+
Ok(BlocksByMilestoneIdPagination {
443+
sort,
444+
page_size: page_size.min(config.max_page_size),
445+
cursor,
446+
})
447+
}
448+
}
449+
333450
#[cfg(test)]
334451
mod test {
335452
use axum::{extract::RequestParts, http::Request};

src/bin/inx-chronicle/api/stardust/explorer/responses.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ pub struct BalanceResponse {
8282

8383
impl_success_response!(BalanceResponse);
8484

85-
/// Response of GET /api/explorer/v2/blocks/{block_id}/children.
86-
/// Returns all children of a specific block.
8785
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
8886
#[serde(rename_all = "camelCase")]
8987
pub struct BlockChildrenResponse {
@@ -104,6 +102,15 @@ pub struct MilestonesResponse {
104102

105103
impl_success_response!(MilestonesResponse);
106104

105+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
106+
#[serde(rename_all = "camelCase")]
107+
pub struct BlocksByMilestoneResponse {
108+
pub blocks: Vec<String>,
109+
pub cursor: Option<String>,
110+
}
111+
112+
impl_success_response!(BlocksByMilestoneResponse);
113+
107114
#[derive(Clone, Debug, Serialize, Deserialize)]
108115
#[serde(rename_all = "camelCase")]
109116
pub struct MilestoneDto {

src/bin/inx-chronicle/api/stardust/explorer/routes.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ use futures::{StreamExt, TryStreamExt};
2020

2121
use super::{
2222
extractors::{
23-
LedgerIndex, LedgerUpdatesByAddressCursor, LedgerUpdatesByAddressPagination, LedgerUpdatesByMilestoneCursor,
23+
BlocksByMilestoneCursor, BlocksByMilestoneIdPagination, BlocksByMilestoneIndexPagination, LedgerIndex,
24+
LedgerUpdatesByAddressCursor, LedgerUpdatesByAddressPagination, LedgerUpdatesByMilestoneCursor,
2425
LedgerUpdatesByMilestonePagination, MilestonesCursor, MilestonesPagination, RichestAddressesQuery,
2526
},
2627
responses::{
27-
AddressStatDto, BalanceResponse, BlockChildrenResponse, LedgerUpdatesByAddressResponse,
28-
LedgerUpdatesByMilestoneResponse, MilestonesResponse, RichestAddressesResponse, TokenDistributionResponse,
28+
AddressStatDto, BalanceResponse, BlockChildrenResponse, BlocksByMilestoneResponse,
29+
LedgerUpdatesByAddressResponse, LedgerUpdatesByMilestoneResponse, MilestonesResponse, RichestAddressesResponse,
30+
TokenDistributionResponse,
2931
},
3032
};
3133
use crate::api::{
@@ -39,7 +41,13 @@ pub fn routes() -> Router {
3941
Router::new()
4042
.route("/balance/:address", get(balance))
4143
.route("/blocks/:block_id/children", get(block_children))
42-
.route("/milestones", get(milestones))
44+
.nest(
45+
"/milestones",
46+
Router::new()
47+
.route("/", get(milestones))
48+
.route("/:milestone_id/blocks", get(blocks_by_milestone_id))
49+
.route("/by-index/:milestone_index/blocks", get(blocks_by_milestone_index)),
50+
)
4351
.nest(
4452
"/ledger",
4553
Router::new()
@@ -223,6 +231,69 @@ async fn milestones(
223231
Ok(MilestonesResponse { items, cursor })
224232
}
225233

234+
async fn blocks_by_milestone_index(
235+
database: Extension<MongoDb>,
236+
Path(milestone_index): Path<MilestoneIndex>,
237+
BlocksByMilestoneIndexPagination {
238+
sort,
239+
page_size,
240+
cursor,
241+
}: BlocksByMilestoneIndexPagination,
242+
) -> ApiResult<BlocksByMilestoneResponse> {
243+
let mut record_stream = database
244+
.collection::<BlockCollection>()
245+
.get_blocks_by_milestone_index(milestone_index, page_size + 1, cursor, sort)
246+
.await?;
247+
248+
// Take all of the requested records first
249+
let blocks = record_stream
250+
.by_ref()
251+
.take(page_size)
252+
.map_ok(|rec| rec.block_id.to_hex())
253+
.try_collect()
254+
.await?;
255+
256+
// If any record is left, use it to make the paging state
257+
let cursor = record_stream.try_next().await?.map(|rec| {
258+
BlocksByMilestoneCursor {
259+
white_flag_index: rec.white_flag_index,
260+
page_size,
261+
}
262+
.to_string()
263+
});
264+
265+
Ok(BlocksByMilestoneResponse { blocks, cursor })
266+
}
267+
268+
async fn blocks_by_milestone_id(
269+
database: Extension<MongoDb>,
270+
Path(milestone_id): Path<String>,
271+
BlocksByMilestoneIdPagination {
272+
sort,
273+
page_size,
274+
cursor,
275+
}: BlocksByMilestoneIdPagination,
276+
) -> ApiResult<BlocksByMilestoneResponse> {
277+
let milestone_id = MilestoneId::from_str(&milestone_id).map_err(RequestError::from)?;
278+
let milestone_index = database
279+
.collection::<MilestoneCollection>()
280+
.get_milestone_payload_by_id(&milestone_id)
281+
.await?
282+
.ok_or(MissingError::NoResults)?
283+
.essence
284+
.index;
285+
blocks_by_milestone_index(
286+
database,
287+
Path(milestone_index),
288+
BlocksByMilestoneIndexPagination {
289+
sort,
290+
page_size,
291+
cursor,
292+
},
293+
)
294+
.await
295+
}
296+
226297
async fn richest_addresses_ledger_analytics(
227298
database: Extension<MongoDb>,
228299
RichestAddressesQuery { top, ledger_index }: RichestAddressesQuery,

0 commit comments

Comments
 (0)