iota_rest_api/
checkpoints.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use axum::extract::{Path, Query, State};
6use iota_sdk2::types::{
7    CheckpointData, CheckpointDigest, CheckpointSequenceNumber, SignedCheckpointSummary,
8};
9use iota_types::storage::ReadStore;
10use tap::Pipe;
11
12use crate::{
13    Direction, Page, RestService, Result,
14    accept::AcceptFormat,
15    openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
16    reader::StateReader,
17    response::ResponseContent,
18};
19
20pub struct GetCheckpointFull;
21
22impl ApiEndpoint<RestService> for GetCheckpointFull {
23    fn method(&self) -> axum::http::Method {
24        axum::http::Method::GET
25    }
26
27    fn path(&self) -> &'static str {
28        "/checkpoints/{checkpoint}/full"
29    }
30
31    fn hidden(&self) -> bool {
32        true
33    }
34
35    fn operation(
36        &self,
37        generator: &mut schemars::gen::SchemaGenerator,
38    ) -> openapiv3::v3_1::Operation {
39        OperationBuilder::new()
40            .tag("Checkpoint")
41            .operation_id("GetCheckpointFull")
42            .path_parameter::<CheckpointSequenceNumber>("checkpoint", generator)
43            .response(
44                200,
45                ResponseBuilder::new()
46                    .json_content::<CheckpointData>(generator)
47                    .bcs_content()
48                    .build(),
49            )
50            .response(404, ResponseBuilder::new().build())
51            .response(410, ResponseBuilder::new().build())
52            .build()
53    }
54
55    fn handler(&self) -> RouteHandler<RestService> {
56        RouteHandler::new(self.method(), get_checkpoint_full)
57    }
58}
59
60async fn get_checkpoint_full(
61    Path(checkpoint_id): Path<CheckpointId>,
62    accept: AcceptFormat,
63    State(state): State<StateReader>,
64) -> Result<ResponseContent<iota_types::full_checkpoint_content::CheckpointData>> {
65    let verified_summary = match checkpoint_id {
66        CheckpointId::SequenceNumber(s) => {
67            // Since we need object contents we need to check for the lowest available
68            // checkpoint with objects that hasn't been pruned
69            let oldest_checkpoint = state.inner().get_lowest_available_checkpoint_objects()?;
70            if s < oldest_checkpoint {
71                return Err(crate::RestError::new(
72                    axum::http::StatusCode::GONE,
73                    "Old checkpoints have been pruned",
74                ));
75            }
76
77            state.inner().get_checkpoint_by_sequence_number(s)
78        }
79        CheckpointId::Digest(d) => state.inner().get_checkpoint_by_digest(&d.into()),
80    }?
81    .ok_or(CheckpointNotFoundError(checkpoint_id))?;
82
83    let checkpoint_contents = state
84        .inner()
85        .get_checkpoint_contents_by_digest(&verified_summary.content_digest)?
86        .ok_or(CheckpointNotFoundError(checkpoint_id))?;
87
88    let checkpoint_data = state
89        .inner()
90        .get_checkpoint_data(verified_summary, checkpoint_contents)?;
91
92    match accept {
93        AcceptFormat::Json => ResponseContent::Json(checkpoint_data),
94        AcceptFormat::Bcs => ResponseContent::Bcs(checkpoint_data),
95    }
96    .pipe(Ok)
97}
98
99pub struct GetCheckpoint;
100
101impl ApiEndpoint<RestService> for GetCheckpoint {
102    fn method(&self) -> axum::http::Method {
103        axum::http::Method::GET
104    }
105
106    fn path(&self) -> &'static str {
107        "/checkpoints/{checkpoint}"
108    }
109
110    fn operation(
111        &self,
112        generator: &mut schemars::gen::SchemaGenerator,
113    ) -> openapiv3::v3_1::Operation {
114        OperationBuilder::new()
115            .tag("Checkpoint")
116            .operation_id("GetCheckpoint")
117            .path_parameter::<CheckpointSequenceNumber>("checkpoint", generator)
118            .response(
119                200,
120                ResponseBuilder::new()
121                    .json_content::<SignedCheckpointSummary>(generator)
122                    .bcs_content()
123                    .build(),
124            )
125            .response(404, ResponseBuilder::new().build())
126            .response(410, ResponseBuilder::new().build())
127            .build()
128    }
129
130    fn handler(&self) -> RouteHandler<RestService> {
131        RouteHandler::new(self.method(), get_checkpoint)
132    }
133}
134
135async fn get_checkpoint(
136    Path(checkpoint_id): Path<CheckpointId>,
137    accept: AcceptFormat,
138    State(state): State<StateReader>,
139) -> Result<ResponseContent<SignedCheckpointSummary>> {
140    let summary = match checkpoint_id {
141        CheckpointId::SequenceNumber(s) => {
142            let oldest_checkpoint = state.inner().get_lowest_available_checkpoint()?;
143            if s < oldest_checkpoint {
144                return Err(crate::RestError::new(
145                    axum::http::StatusCode::GONE,
146                    "Old checkpoints have been pruned",
147                ));
148            }
149
150            state.inner().get_checkpoint_by_sequence_number(s)
151        }
152        CheckpointId::Digest(d) => state.inner().get_checkpoint_by_digest(&d.into()),
153    }?
154    .ok_or(CheckpointNotFoundError(checkpoint_id))?
155    .into_inner()
156    .try_into()?;
157
158    match accept {
159        AcceptFormat::Json => ResponseContent::Json(summary),
160        AcceptFormat::Bcs => ResponseContent::Bcs(summary),
161    }
162    .pipe(Ok)
163}
164
165#[derive(Debug, Copy, Clone, Eq, PartialEq)]
166pub enum CheckpointId {
167    SequenceNumber(CheckpointSequenceNumber),
168    Digest(CheckpointDigest),
169}
170
171impl<'de> serde::Deserialize<'de> for CheckpointId {
172    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
173    where
174        D: serde::Deserializer<'de>,
175    {
176        let raw = String::deserialize(deserializer)?;
177
178        if let Ok(s) = raw.parse::<CheckpointSequenceNumber>() {
179            Ok(Self::SequenceNumber(s))
180        } else if let Ok(d) = raw.parse::<CheckpointDigest>() {
181            Ok(Self::Digest(d))
182        } else {
183            Err(serde::de::Error::custom(format!(
184                "unrecognized checkpoint-id {raw}"
185            )))
186        }
187    }
188}
189
190impl serde::Serialize for CheckpointId {
191    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
192    where
193        S: serde::Serializer,
194    {
195        match self {
196            CheckpointId::SequenceNumber(s) => serializer.serialize_str(&s.to_string()),
197            CheckpointId::Digest(d) => serializer.serialize_str(&d.to_string()),
198        }
199    }
200}
201
202#[derive(Debug)]
203pub struct CheckpointNotFoundError(CheckpointId);
204
205impl std::fmt::Display for CheckpointNotFoundError {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        write!(f, "Checkpoint ")?;
208
209        match self.0 {
210            CheckpointId::SequenceNumber(n) => write!(f, "{n}")?,
211            CheckpointId::Digest(d) => write!(f, "{d}")?,
212        }
213
214        write!(f, " not found")
215    }
216}
217
218impl std::error::Error for CheckpointNotFoundError {}
219
220impl From<CheckpointNotFoundError> for crate::RestError {
221    fn from(value: CheckpointNotFoundError) -> Self {
222        Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
223    }
224}
225
226pub struct ListCheckpoints;
227
228impl ApiEndpoint<RestService> for ListCheckpoints {
229    fn method(&self) -> axum::http::Method {
230        axum::http::Method::GET
231    }
232
233    fn path(&self) -> &'static str {
234        "/checkpoints"
235    }
236
237    fn operation(
238        &self,
239        generator: &mut schemars::gen::SchemaGenerator,
240    ) -> openapiv3::v3_1::Operation {
241        OperationBuilder::new()
242            .tag("Checkpoint")
243            .operation_id("ListCheckpoints")
244            .query_parameters::<ListCheckpointsQueryParameters>(generator)
245            .response(
246                200,
247                ResponseBuilder::new()
248                    .json_content::<Vec<SignedCheckpointSummary>>(generator)
249                    .bcs_content()
250                    .header::<String>(crate::types::X_IOTA_CURSOR, generator)
251                    .build(),
252            )
253            .response(410, ResponseBuilder::new().build())
254            .build()
255    }
256
257    fn handler(&self) -> RouteHandler<RestService> {
258        RouteHandler::new(self.method(), list_checkpoints)
259    }
260}
261
262async fn list_checkpoints(
263    Query(parameters): Query<ListCheckpointsQueryParameters>,
264    accept: AcceptFormat,
265    State(state): State<StateReader>,
266) -> Result<Page<SignedCheckpointSummary, CheckpointSequenceNumber>> {
267    let latest_checkpoint = state.inner().get_latest_checkpoint()?.sequence_number;
268    let oldest_checkpoint = state.inner().get_lowest_available_checkpoint()?;
269    let limit = parameters.limit();
270    let start = parameters.start(latest_checkpoint);
271    let direction = parameters.direction();
272
273    if start < oldest_checkpoint {
274        return Err(crate::RestError::new(
275            axum::http::StatusCode::GONE,
276            "Old checkpoints have been pruned",
277        ));
278    }
279
280    let checkpoints = state
281        .checkpoint_iter(direction, start)
282        .take(limit)
283        .map(|result| {
284            result
285                .map_err(Into::into)
286                .and_then(|(checkpoint, _contents)| {
287                    SignedCheckpointSummary::try_from(checkpoint).map_err(Into::into)
288                })
289        })
290        .collect::<Result<Vec<_>>>()?;
291
292    let cursor = checkpoints.last().and_then(|checkpoint| match direction {
293        Direction::Ascending => checkpoint.checkpoint.sequence_number.checked_add(1),
294        Direction::Descending => checkpoint.checkpoint.sequence_number.checked_sub(1),
295    });
296
297    match accept {
298        AcceptFormat::Json => ResponseContent::Json(checkpoints),
299        AcceptFormat::Bcs => ResponseContent::Bcs(checkpoints),
300    }
301    .pipe(|entries| Page { entries, cursor })
302    .pipe(Ok)
303}
304
305#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
306pub struct ListCheckpointsQueryParameters {
307    pub limit: Option<u32>,
308    /// The checkpoint to start listing from.
309    ///
310    /// Defaults to the latest checkpoint if not provided.
311    pub start: Option<CheckpointSequenceNumber>,
312    pub direction: Option<Direction>,
313}
314
315impl ListCheckpointsQueryParameters {
316    pub fn limit(&self) -> usize {
317        self.limit
318            .map(|l| (l as usize).clamp(1, crate::MAX_PAGE_SIZE))
319            .unwrap_or(crate::DEFAULT_PAGE_SIZE)
320    }
321
322    pub fn start(&self, default: CheckpointSequenceNumber) -> CheckpointSequenceNumber {
323        self.start.unwrap_or(default)
324    }
325
326    pub fn direction(&self) -> Direction {
327        self.direction.unwrap_or(Direction::Descending)
328    }
329}