1use 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 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 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}