1use axum::extract::{Path, Query, State};
6use iota_sdk2::types::{
7 CheckpointContents, CheckpointDigest, CheckpointSequenceNumber, CheckpointSummary,
8 SignedCheckpointSummary, ValidatorAggregatedSignature,
9};
10use iota_types::storage::ReadStore;
11use tap::Pipe;
12
13use crate::{
14 Direction, Page, RestError, RestService, Result,
15 accept::AcceptFormat,
16 openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
17 reader::StateReader,
18 response::{Bcs, ResponseContent},
19};
20
21#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
22pub struct CheckpointResponse {
23 pub checkpoint: CheckpointSummary,
24 pub signature: ValidatorAggregatedSignature,
25 pub contents: Option<CheckpointContents>,
26}
27
28pub struct GetCheckpoint;
36
37impl ApiEndpoint<RestService> for GetCheckpoint {
38 fn method(&self) -> axum::http::Method {
39 axum::http::Method::GET
40 }
41
42 fn path(&self) -> &'static str {
43 "/checkpoints/{checkpoint}"
44 }
45
46 fn stable(&self) -> bool {
47 true
48 }
49
50 fn operation(
51 &self,
52 generator: &mut schemars::gen::SchemaGenerator,
53 ) -> openapiv3::v3_1::Operation {
54 OperationBuilder::new()
55 .tag("Checkpoint")
56 .operation_id("Get Checkpoint")
57 .path_parameter::<CheckpointId>("checkpoint", generator)
58 .query_parameters::<GetCheckpointQueryParameters>(generator)
59 .response(
60 200,
61 ResponseBuilder::new()
62 .json_content::<CheckpointResponse>(generator)
63 .bcs_content()
64 .build(),
65 )
66 .response(404, ResponseBuilder::new().build())
67 .response(410, ResponseBuilder::new().build())
68 .response(500, ResponseBuilder::new().build())
69 .build()
70 }
71
72 fn handler(&self) -> RouteHandler<RestService> {
73 RouteHandler::new(self.method(), get_checkpoint)
74 }
75}
76
77async fn get_checkpoint(
78 Path(checkpoint_id): Path<CheckpointId>,
79 Query(parameters): Query<GetCheckpointQueryParameters>,
80 accept: AcceptFormat,
81 State(state): State<StateReader>,
82) -> Result<ResponseContent<CheckpointResponse>> {
83 let SignedCheckpointSummary {
84 checkpoint,
85 signature,
86 } = match checkpoint_id {
87 CheckpointId::SequenceNumber(s) => {
88 let oldest_checkpoint = state.inner().get_lowest_available_checkpoint()?;
89 if s < oldest_checkpoint {
90 return Err(crate::RestError::new(
91 axum::http::StatusCode::GONE,
92 "Old checkpoints have been pruned",
93 ));
94 }
95
96 state.inner().get_checkpoint_by_sequence_number(s)
97 }
98 CheckpointId::Digest(d) => state.inner().get_checkpoint_by_digest(&d.into()),
99 }?
100 .ok_or(CheckpointNotFoundError(checkpoint_id))?
101 .into_inner()
102 .try_into()?;
103
104 let contents = if parameters.contents {
105 Some(
106 state
107 .inner()
108 .get_checkpoint_contents_by_sequence_number(checkpoint.sequence_number)?
109 .ok_or(CheckpointNotFoundError(checkpoint_id))?
110 .try_into()?,
111 )
112 } else {
113 None
114 };
115
116 let response = CheckpointResponse {
117 checkpoint,
118 signature,
119 contents,
120 };
121
122 match accept {
123 AcceptFormat::Json => ResponseContent::Json(response),
124 AcceptFormat::Bcs => ResponseContent::Bcs(response),
125 }
126 .pipe(Ok)
127}
128
129#[derive(Debug, Copy, Clone, Eq, PartialEq, schemars::JsonSchema)]
130#[schemars(untagged)]
131pub enum CheckpointId {
132 #[schemars(
133 title = "SequenceNumber",
134 example = "CheckpointSequenceNumber::default"
135 )]
136 SequenceNumber(#[schemars(with = "crate::_schemars::U64")] CheckpointSequenceNumber),
138 #[schemars(title = "Digest", example = "example_digest")]
139 Digest(CheckpointDigest),
141}
142
143fn example_digest() -> CheckpointDigest {
144 "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S"
145 .parse()
146 .unwrap()
147}
148
149impl<'de> serde::Deserialize<'de> for CheckpointId {
150 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
151 where
152 D: serde::Deserializer<'de>,
153 {
154 let raw = String::deserialize(deserializer)?;
155
156 if let Ok(s) = raw.parse::<CheckpointSequenceNumber>() {
157 Ok(Self::SequenceNumber(s))
158 } else if let Ok(d) = raw.parse::<CheckpointDigest>() {
159 Ok(Self::Digest(d))
160 } else {
161 Err(serde::de::Error::custom(format!(
162 "unrecognized checkpoint-id {raw}"
163 )))
164 }
165 }
166}
167
168impl serde::Serialize for CheckpointId {
169 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
170 where
171 S: serde::Serializer,
172 {
173 match self {
174 CheckpointId::SequenceNumber(s) => serializer.serialize_str(&s.to_string()),
175 CheckpointId::Digest(d) => serializer.serialize_str(&d.to_string()),
176 }
177 }
178}
179
180#[derive(Debug)]
181pub struct CheckpointNotFoundError(CheckpointId);
182
183impl std::fmt::Display for CheckpointNotFoundError {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 write!(f, "Checkpoint ")?;
186
187 match self.0 {
188 CheckpointId::SequenceNumber(n) => write!(f, "{n}")?,
189 CheckpointId::Digest(d) => write!(f, "{d}")?,
190 }
191
192 write!(f, " not found")
193 }
194}
195
196impl std::error::Error for CheckpointNotFoundError {}
197
198impl From<CheckpointNotFoundError> for crate::RestError {
199 fn from(value: CheckpointNotFoundError) -> Self {
200 Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
201 }
202}
203
204#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
206pub struct GetCheckpointQueryParameters {
207 #[serde(default)]
209 pub contents: bool,
210}
211
212pub struct ListCheckpoints;
220
221impl ApiEndpoint<RestService> for ListCheckpoints {
222 fn method(&self) -> axum::http::Method {
223 axum::http::Method::GET
224 }
225
226 fn path(&self) -> &'static str {
227 "/checkpoints"
228 }
229
230 fn stable(&self) -> bool {
231 true
232 }
233
234 fn operation(
235 &self,
236 generator: &mut schemars::gen::SchemaGenerator,
237 ) -> openapiv3::v3_1::Operation {
238 OperationBuilder::new()
239 .tag("Checkpoint")
240 .operation_id("List Checkpoints")
241 .query_parameters::<ListCheckpointsQueryParameters>(generator)
242 .response(
243 200,
244 ResponseBuilder::new()
245 .json_content::<Vec<CheckpointResponse>>(generator)
246 .bcs_content()
247 .header::<String>(crate::types::X_IOTA_CURSOR, generator)
248 .build(),
249 )
250 .response(410, ResponseBuilder::new().build())
251 .response(500, ResponseBuilder::new().build())
252 .build()
253 }
254
255 fn handler(&self) -> RouteHandler<RestService> {
256 RouteHandler::new(self.method(), list_checkpoints)
257 }
258}
259
260async fn list_checkpoints(
261 Query(parameters): Query<ListCheckpointsQueryParameters>,
262 accept: AcceptFormat,
263 State(state): State<StateReader>,
264) -> Result<Page<CheckpointResponse, CheckpointSequenceNumber>> {
265 let latest_checkpoint = state.inner().get_latest_checkpoint()?.sequence_number;
266 let oldest_checkpoint = state.inner().get_lowest_available_checkpoint()?;
267 let limit = parameters.limit();
268 let start = parameters.start(latest_checkpoint);
269 let direction = parameters.direction();
270
271 if start < oldest_checkpoint {
272 return Err(crate::RestError::new(
273 axum::http::StatusCode::GONE,
274 "Old checkpoints have been pruned",
275 ));
276 }
277
278 let checkpoints = state
279 .checkpoint_iter(direction, start)
280 .take(limit)
281 .map(|result| {
282 result
283 .map_err(Into::into)
284 .and_then(|(checkpoint, contents)| {
285 let SignedCheckpointSummary {
286 checkpoint,
287 signature,
288 } = checkpoint.try_into()?;
289 let contents = if parameters.contents {
290 Some(contents.try_into()?)
291 } else {
292 None
293 };
294 Ok(CheckpointResponse {
295 checkpoint,
296 signature,
297 contents,
298 })
299 })
300 })
301 .collect::<Result<Vec<_>>>()?;
302
303 let cursor = checkpoints.last().and_then(|checkpoint| match direction {
304 Direction::Ascending => checkpoint.checkpoint.sequence_number.checked_add(1),
305 Direction::Descending => {
306 let cursor = checkpoint.checkpoint.sequence_number.checked_sub(1);
307 if cursor < Some(oldest_checkpoint) {
310 None
311 } else {
312 cursor
313 }
314 }
315 });
316
317 match accept {
318 AcceptFormat::Json => ResponseContent::Json(checkpoints),
319 AcceptFormat::Bcs => ResponseContent::Bcs(checkpoints),
320 }
321 .pipe(|entries| Page { entries, cursor })
322 .pipe(Ok)
323}
324
325#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
326pub struct ListCheckpointsQueryParameters {
327 pub limit: Option<u32>,
331 pub start: Option<CheckpointSequenceNumber>,
335 pub direction: Option<Direction>,
339 #[serde(default)]
341 pub contents: bool,
342}
343
344impl ListCheckpointsQueryParameters {
345 pub fn limit(&self) -> usize {
346 self.limit
347 .map(|l| (l as usize).clamp(1, crate::MAX_PAGE_SIZE))
348 .unwrap_or(crate::DEFAULT_PAGE_SIZE)
349 }
350
351 pub fn start(&self, default: CheckpointSequenceNumber) -> CheckpointSequenceNumber {
352 self.start.unwrap_or(default)
353 }
354
355 pub fn direction(&self) -> Direction {
356 self.direction.unwrap_or(Direction::Descending)
357 }
358}
359
360pub struct GetFullCheckpoint;
371
372impl ApiEndpoint<RestService> for GetFullCheckpoint {
373 fn method(&self) -> axum::http::Method {
374 axum::http::Method::GET
375 }
376
377 fn path(&self) -> &'static str {
378 "/checkpoints/{checkpoint}/full"
379 }
380
381 fn stable(&self) -> bool {
382 false
386 }
387
388 fn operation(
389 &self,
390 generator: &mut schemars::gen::SchemaGenerator,
391 ) -> openapiv3::v3_1::Operation {
392 OperationBuilder::new()
393 .tag("Checkpoint")
394 .operation_id("Get Full Checkpoint")
395 .path_parameter::<CheckpointId>("checkpoint", generator)
396 .response(200, ResponseBuilder::new().bcs_content().build())
397 .response(404, ResponseBuilder::new().build())
398 .response(410, ResponseBuilder::new().build())
399 .response(500, ResponseBuilder::new().build())
400 .build()
401 }
402
403 fn handler(&self) -> RouteHandler<RestService> {
404 RouteHandler::new(self.method(), get_full_checkpoint)
405 }
406}
407
408async fn get_full_checkpoint(
409 Path(checkpoint_id): Path<CheckpointId>,
410 accept: AcceptFormat,
411 State(state): State<StateReader>,
412) -> Result<Bcs<iota_types::full_checkpoint_content::CheckpointData>> {
413 match accept {
414 AcceptFormat::Bcs => {}
415 _ => {
416 return Err(RestError::new(
417 axum::http::StatusCode::BAD_REQUEST,
418 "invalid accept type; only 'application/bcs' is supported",
419 ));
420 }
421 }
422
423 let verified_summary = match checkpoint_id {
424 CheckpointId::SequenceNumber(s) => {
425 let oldest_checkpoint = state.inner().get_lowest_available_checkpoint_objects()?;
428 if s < oldest_checkpoint {
429 return Err(crate::RestError::new(
430 axum::http::StatusCode::GONE,
431 "Old checkpoints have been pruned",
432 ));
433 }
434
435 state.inner().get_checkpoint_by_sequence_number(s)
436 }
437 CheckpointId::Digest(d) => state.inner().get_checkpoint_by_digest(&d.into()),
438 }?
439 .ok_or(CheckpointNotFoundError(checkpoint_id))?;
440
441 let checkpoint_contents = state
442 .inner()
443 .get_checkpoint_contents_by_digest(&verified_summary.content_digest)?
444 .ok_or(CheckpointNotFoundError(checkpoint_id))?;
445
446 let checkpoint_data = state
447 .inner()
448 .get_checkpoint_data(verified_summary, checkpoint_contents)?;
449
450 Ok(Bcs(checkpoint_data))
451}
452
453pub struct ListFullCheckpoints;
464
465impl ApiEndpoint<RestService> for ListFullCheckpoints {
466 fn method(&self) -> axum::http::Method {
467 axum::http::Method::GET
468 }
469
470 fn path(&self) -> &'static str {
471 "/checkpoints/full"
472 }
473
474 fn stable(&self) -> bool {
475 false
479 }
480
481 fn operation(
482 &self,
483 generator: &mut schemars::gen::SchemaGenerator,
484 ) -> openapiv3::v3_1::Operation {
485 OperationBuilder::new()
486 .tag("Checkpoint")
487 .operation_id("List Full Checkpoints")
488 .query_parameters::<ListFullCheckpointsQueryParameters>(generator)
489 .response(200, ResponseBuilder::new().bcs_content().build())
490 .response(410, ResponseBuilder::new().build())
491 .response(500, ResponseBuilder::new().build())
492 .build()
493 }
494
495 fn handler(&self) -> RouteHandler<RestService> {
496 RouteHandler::new(self.method(), list_full_checkpoints)
497 }
498}
499
500async fn list_full_checkpoints(
501 Query(parameters): Query<ListFullCheckpointsQueryParameters>,
502 accept: AcceptFormat,
503 State(state): State<StateReader>,
504) -> Result<Page<iota_types::full_checkpoint_content::CheckpointData, CheckpointSequenceNumber>> {
505 match accept {
506 AcceptFormat::Bcs => {}
507 _ => {
508 return Err(RestError::new(
509 axum::http::StatusCode::BAD_REQUEST,
510 "invalid accept type; only 'application/bcs' is supported",
511 ));
512 }
513 }
514
515 let latest_checkpoint = state.inner().get_latest_checkpoint()?.sequence_number;
516 let oldest_checkpoint = state.inner().get_lowest_available_checkpoint_objects()?;
517 let limit = parameters.limit();
518 let start = parameters.start(latest_checkpoint);
519 let direction = parameters.direction();
520
521 if start < oldest_checkpoint {
522 return Err(crate::RestError::new(
523 axum::http::StatusCode::GONE,
524 "Old checkpoints have been pruned",
525 ));
526 }
527
528 let checkpoints = state
529 .checkpoint_iter(direction, start)
530 .take(
531 if direction.is_descending() {
533 std::cmp::min(limit, start.saturating_sub(oldest_checkpoint) as usize)
534 } else {
535 limit
536 },
537 )
538 .map(|result| {
539 result
540 .map_err(Into::into)
541 .and_then(|(checkpoint, contents)| {
542 state
543 .inner()
544 .get_checkpoint_data(
545 iota_types::messages_checkpoint::VerifiedCheckpoint::new_from_verified(
546 checkpoint,
547 ),
548 contents,
549 )
550 .map_err(Into::into)
551 })
552 })
553 .collect::<Result<Vec<_>>>()?;
554
555 let cursor = checkpoints.last().and_then(|checkpoint| match direction {
556 Direction::Ascending => checkpoint.checkpoint_summary.sequence_number.checked_add(1),
557 Direction::Descending => {
558 let cursor = checkpoint.checkpoint_summary.sequence_number.checked_sub(1);
559 if cursor < Some(oldest_checkpoint) {
562 None
563 } else {
564 cursor
565 }
566 }
567 });
568
569 ResponseContent::Bcs(checkpoints)
570 .pipe(|entries| Page { entries, cursor })
571 .pipe(Ok)
572}
573
574#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
575pub struct ListFullCheckpointsQueryParameters {
576 pub limit: Option<u32>,
580 pub start: Option<CheckpointSequenceNumber>,
584 pub direction: Option<Direction>,
588}
589
590impl ListFullCheckpointsQueryParameters {
591 pub fn limit(&self) -> usize {
592 self.limit.map(|l| (l as usize).clamp(1, 10)).unwrap_or(5)
593 }
594
595 pub fn start(&self, default: CheckpointSequenceNumber) -> CheckpointSequenceNumber {
596 self.start.unwrap_or(default)
597 }
598
599 pub fn direction(&self) -> Direction {
600 self.direction.unwrap_or(Direction::Descending)
601 }
602}