Skip to content

Commit

Permalink
feat(api): add milestones endpoint to explorer API (#666)
Browse files Browse the repository at this point in the history
feat(api): add milestones endpoint to explorer API (#633)

* Add milestones endpoint

Co-authored-by: Alexandcoats <alexandcoats@gmail.com>
  • Loading branch information
grtlr and Alexandcoats authored Sep 13, 2022
1 parent 5c697f3 commit 3d221bf
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 78 deletions.
75 changes: 75 additions & 0 deletions documentation/api/api-explorer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ tags:
description: Everything about blocks.
- name: ledger
description: Everything about the ledger.
- name: milestones
description: Everything about milestones.
paths:
/api/explorer/v2/balance/{address}:
get:
Expand Down Expand Up @@ -74,6 +76,35 @@ paths:
$ref: "#/components/responses/NoResults"
"500":
$ref: "#/components/responses/InternalError"
/api/explorer/v2/milestones:
get:
tags:
- milestones
summary: Returns milestones based on given query parameters.
description: >-
Returns a list of milestones matching provided query parameters.
parameters:
- $ref: "#/components/parameters/startTimestamp"
- $ref: "#/components/parameters/endTimestamp"
- $ref: "#/components/parameters/sort"
- $ref: "#/components/parameters/pageSize"
- $ref: "#/components/parameters/cursor"
responses:
"200":
description: Successful operation.
content:
application/json:
schema:
$ref: "#/components/schemas/MilestonesResponse"
examples:
default:
$ref: "#/components/examples/milestones-example"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NoResults"
"500":
$ref: "#/components/responses/InternalError"
/api/explorer/v2/ledger/updates/by-address/{address}:
get:
tags:
Expand Down Expand Up @@ -232,6 +263,28 @@ components:
required:
- address
- items
MilestonesResponse:
description: Paged milestones.
properties:
items:
type: array
description: A list of milestones.
items:
properties:
milestoneId:
type: string
description: The milestone ID.
index:
type: integer
description: The index of the milestone.
required:
- milestoneId
- index
cursor:
type: string
description: The cursor which can be used to retrieve the next logical page of results.
required:
- items
responses:
NoResults:
description: >-
Expand Down Expand Up @@ -297,6 +350,20 @@ components:
description: >-
The milestone index at which to start retrieving results. This will be overridden
by the cursor if provided.
startTimestamp:
in: query
name: startTimestamp
schema:
type: integer
example: 1662139730
description: Start timestamp for filtering.
endTimestamp:
in: query
name: endTimestamp
schema:
type: integer
example: 1662139830
description: End timestamp for filtering.
cursor:
in: query
name: cursor
Expand Down Expand Up @@ -334,3 +401,11 @@ components:
outputId: fa0de75d225cca2799395e5fc340702fc7eac821d2bdd79911126f131ae097a20000
isSpent: true
cursor: fa0de75d225cca2799395e5fc340702fc7eac821d2bdd79911126f131ae097a20100.true.100
milestones-example:
value:
items:
- milestoneId: "0x7a09324557e9200f39bf493fc8fd6ac43e9ca750c6f6d884cc72386ddcb7d695"
index: 100
- milestoneId: "0xfa0de75d225cca2799395e5fc340702fc7eac821d2bdd79911126f131ae097a2"
index: 101
cursor: 102.2
38 changes: 0 additions & 38 deletions src/bin/inx-chronicle/api/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,41 +31,3 @@ pub struct Unlock {
pub block_id: String,
pub block: Value,
}

/// An aggregation type that represents the ranges of completed milestones and gaps.
#[cfg(feature = "stardust")]
mod stardust {

use chronicle::{
db::collections::SyncData,
types::{ledger::LedgerInclusionState, tangle::MilestoneIndex},
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SyncDataDto(pub SyncData);

impl_success_response!(SyncDataDto);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Record {
pub id: String,
pub inclusion_state: Option<LedgerInclusionState>,
pub milestone_index: Option<MilestoneIndex>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transfer {
pub transaction_id: String,
pub output_index: u16,
pub is_spending: bool,
pub inclusion_state: Option<LedgerInclusionState>,
pub block_id: String,
pub amount: u64,
}
}

#[cfg(feature = "stardust")]
pub use stardust::*;
87 changes: 86 additions & 1 deletion src/bin/inx-chronicle/api/stardust/explorer/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use axum::{
};
use chronicle::{
db::collections::SortOrder,
types::{stardust::block::output::OutputId, tangle::MilestoneIndex},
types::{
stardust::{block::output::OutputId, milestone::MilestoneTimestamp},
tangle::MilestoneIndex,
},
};
use serde::Deserialize;

Expand Down Expand Up @@ -173,6 +176,88 @@ impl<B: Send> FromRequest<B> for LedgerUpdatesByMilestonePagination {
}
}

pub struct MilestonesPagination {
pub start_timestamp: Option<MilestoneTimestamp>,
pub end_timestamp: Option<MilestoneTimestamp>,
pub sort: SortOrder,
pub page_size: usize,
pub cursor: Option<MilestoneIndex>,
}

#[derive(Clone, Deserialize, Default)]
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
pub struct MilestonesPaginationQuery {
pub start_timestamp: Option<u32>,
pub end_timestamp: Option<u32>,
pub sort: Option<String>,
pub page_size: Option<usize>,
pub cursor: Option<String>,
}

#[derive(Clone)]
pub struct MilestonesCursor {
pub milestone_index: MilestoneIndex,
pub page_size: usize,
}

impl FromStr for MilestonesCursor {
type Err = ApiError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<_> = s.split('.').collect();
Ok(match parts[..] {
[m, ps] => MilestonesCursor {
milestone_index: m.parse().map_err(ApiError::bad_parse)?,
page_size: ps.parse().map_err(ApiError::bad_parse)?,
},
_ => return Err(ApiError::bad_parse(ParseError::BadPagingState)),
})
}
}

impl Display for MilestonesCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.milestone_index, self.page_size)
}
}

#[async_trait]
impl<B: Send> FromRequest<B> for MilestonesPagination {
type Rejection = ApiError;

async fn from_request(req: &mut axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
let Query(query) = Query::<MilestonesPaginationQuery>::from_request(req)
.await
.map_err(ApiError::QueryError)?;
let Extension(config) = Extension::<ApiData>::from_request(req).await?;

if matches!((query.start_timestamp, query.end_timestamp), (Some(start), Some(end)) if end < start) {
return Err(ApiError::BadTimeRange);
}

let sort = query
.sort
.as_deref()
.map_or(Ok(Default::default()), str::parse)
.map_err(ParseError::SortOrder)?;

let (page_size, cursor) = if let Some(cursor) = query.cursor {
let cursor: MilestonesCursor = cursor.parse()?;
(cursor.page_size, Some(cursor.milestone_index))
} else {
(query.page_size.unwrap_or(DEFAULT_PAGE_SIZE), None)
};

Ok(MilestonesPagination {
start_timestamp: query.start_timestamp.map(Into::into),
end_timestamp: query.end_timestamp.map(Into::into),
sort,
page_size: page_size.min(config.max_page_size),
cursor,
})
}
}

#[cfg(test)]
mod test {
use axum::{extract::RequestParts, http::Request};
Expand Down
27 changes: 26 additions & 1 deletion src/bin/inx-chronicle/api/stardust/explorer/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

use chronicle::{
db::collections::{LedgerUpdateByAddressRecord, LedgerUpdateByMilestoneRecord},
db::collections::{LedgerUpdateByAddressRecord, LedgerUpdateByMilestoneRecord, MilestoneResult},
types::{
stardust::{block::Address, milestone::MilestoneTimestamp},
tangle::MilestoneIndex,
Expand Down Expand Up @@ -92,3 +92,28 @@ pub struct BlockChildrenResponse {
}

impl_success_response!(BlockChildrenResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MilestonesResponse {
pub items: Vec<MilestoneDto>,
pub cursor: Option<String>,
}

impl_success_response!(MilestonesResponse);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MilestoneDto {
milestone_id: String,
index: MilestoneIndex,
}

impl From<MilestoneResult> for MilestoneDto {
fn from(res: MilestoneResult) -> Self {
Self {
milestone_id: res.milestone_id.to_hex(),
index: res.index,
}
}
}
39 changes: 38 additions & 1 deletion src/bin/inx-chronicle/api/stardust/explorer/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ use futures::{StreamExt, TryStreamExt};
use super::{
extractors::{
LedgerUpdatesByAddressCursor, LedgerUpdatesByAddressPagination, LedgerUpdatesByMilestoneCursor,
LedgerUpdatesByMilestonePagination,
LedgerUpdatesByMilestonePagination, MilestonesCursor, MilestonesPagination,
},
responses::{
BalanceResponse, BlockChildrenResponse, LedgerUpdatesByAddressResponse, LedgerUpdatesByMilestoneResponse,
MilestonesResponse,
},
};
use crate::api::{extractors::Pagination, ApiError, ApiResult};
Expand All @@ -28,6 +29,7 @@ pub fn routes() -> Router {
Router::new()
.route("/balance/:address", get(balance))
.route("/blocks/:block_id/children", get(block_children))
.route("/milestones", get(milestones))
.nest(
"/ledger/updates",
Router::new()
Expand Down Expand Up @@ -164,3 +166,38 @@ async fn block_children(
children,
})
}

async fn milestones(
database: Extension<MongoDb>,
MilestonesPagination {
start_timestamp,
end_timestamp,
sort,
page_size,
cursor,
}: MilestonesPagination,
) -> ApiResult<MilestonesResponse> {
let mut record_stream = database
.collection::<MilestoneCollection>()
.get_milestones(start_timestamp, end_timestamp, sort, page_size + 1, cursor)
.await?;

// Take all of the requested records first
let items = record_stream
.by_ref()
.take(page_size)
.map_ok(Into::into)
.try_collect()
.await?;

// If any record is left, use it to make the paging state
let cursor = record_stream.try_next().await?.map(|rec| {
MilestonesCursor {
milestone_index: rec.index,
page_size,
}
.to_string()
});

Ok(MilestonesResponse { items, cursor })
}
Loading

0 comments on commit 3d221bf

Please sign in to comment.