1use crate::admin::AppState;
14use axum::{
15 Json,
16 extract::{Query, State},
17 http::{HeaderMap, HeaderValue, StatusCode},
18 response::{IntoResponse, Response},
19};
20use base64::Engine;
21use consensus_core::{
22 CommitAPI as _, CommitRange,
23 storage::{Store as ConsensusStore, rocksdb_store::RocksDBStore},
24};
25use serde::{Deserialize, Serialize};
26use serde_json::value::Value as JsonValue;
27use std::sync::Arc;
28use sui_types::{
29 base_types::EpochId,
30 digests::{
31 CheckpointContentsDigest, CheckpointDigest, TransactionDigest, TransactionEffectsDigest,
32 },
33 messages_checkpoint::CheckpointSequenceNumber,
34};
35
36pub const DEFAULT_LIMIT: usize = 30;
37
38#[derive(Debug, Deserialize)]
39pub struct LsParams {
40 pub path: String,
41 #[serde(default = "default_limit")]
42 pub limit: usize,
43 #[serde(default)]
45 pub cursor: bool,
46}
47
48#[derive(Debug, Deserialize)]
49pub struct ReadParams {
50 pub path: String,
51 #[serde(default = "default_format")]
52 pub format: ReadFormat,
53}
54
55#[derive(Debug, Deserialize)]
56pub struct DeleteParams {
57 pub path: String,
58}
59
60#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
61#[serde(rename_all = "kebab-case")]
62pub enum ReadFormat {
63 Json,
64 Debug,
65 Bcs,
66 RawBcs,
67}
68
69fn default_limit() -> usize {
70 DEFAULT_LIMIT
71}
72
73fn default_format() -> ReadFormat {
74 ReadFormat::Json
75}
76
77#[derive(Debug, Serialize)]
78pub struct DirEntry {
79 pub name: String,
80 pub is_dir: bool,
81}
82
83pub(crate) struct ApiError(StatusCode, String);
86
87impl<E: std::fmt::Display> From<(StatusCode, E)> for ApiError {
88 fn from((status, e): (StatusCode, E)) -> Self {
89 ApiError(status, e.to_string())
90 }
91}
92
93impl IntoResponse for ApiError {
94 fn into_response(self) -> Response {
95 (self.0, self.1).into_response()
96 }
97}
98
99fn bad_request(msg: impl std::fmt::Display) -> ApiError {
100 ApiError(StatusCode::BAD_REQUEST, msg.to_string())
101}
102
103fn not_found(msg: impl std::fmt::Display) -> ApiError {
104 ApiError(StatusCode::NOT_FOUND, msg.to_string())
105}
106
107fn internal(msg: impl std::fmt::Display) -> ApiError {
108 ApiError(StatusCode::INTERNAL_SERVER_ERROR, msg.to_string())
109}
110
111fn not_implemented(msg: impl std::fmt::Display) -> ApiError {
112 ApiError(StatusCode::NOT_IMPLEMENTED, msg.to_string())
113}
114
115#[derive(Debug)]
118enum VfsPath {
119 Root,
120 Epochs,
121 Epoch(EpochId),
122 EpochFirstCheckpoint(EpochId),
123 EpochLastCheckpoint(EpochId),
124 EpochCommittee(EpochId),
125 EpochCheckpoints(EpochId),
126 EpochCheckpointBySeq(EpochId, CheckpointSequenceNumber),
127 EpochCheckpointByDigest(EpochId, CheckpointDigest),
128 CheckpointsRoot,
129 CheckpointsSeqRoot,
130 CheckpointsBySeq(CheckpointSequenceNumber),
131 CheckpointSeqSummary(CheckpointSequenceNumber),
132 CheckpointSeqContents(CheckpointSequenceNumber),
133 CheckpointSeqContentsShort(CheckpointSequenceNumber),
134 CheckpointsDigestRoot,
135 CheckpointsByDigest(CheckpointDigest),
136 CheckpointDigestSummary(CheckpointDigest),
137 CheckpointDigestContents(CheckpointDigest),
138 CheckpointDigestContentsShort(CheckpointDigest),
139 CheckpointContentsRoot,
140 CheckpointContentsEntry(CheckpointContentsDigest),
141 TransactionsRoot,
142 TransactionEntry(TransactionDigest),
143 TransactionEffectsEntry(TransactionDigest, TransactionEffectsDigest),
144 ConsensusRoot,
145 ConsensusLatest,
146 ConsensusCommitsRoot,
147 ConsensusCommitDir(u32),
148 ConsensusCommitSummary(u32),
149}
150
151fn parse_path(s: &str) -> Result<VfsPath, ApiError> {
152 let parts: Vec<&str> = s
153 .trim_start_matches('/')
154 .split('/')
155 .filter(|p| !p.is_empty())
156 .collect();
157
158 let r = match parts.as_slice() {
159 [] => VfsPath::Root,
160 ["epochs"] => VfsPath::Epochs,
161 ["epochs", e] => VfsPath::Epoch(
162 e.parse()
163 .map_err(|_| bad_request(format!("invalid epoch: {e}")))?,
164 ),
165 ["epochs", e, "first-checkpoint"] => VfsPath::EpochFirstCheckpoint(
166 e.parse()
167 .map_err(|_| bad_request(format!("invalid epoch: {e}")))?,
168 ),
169 ["epochs", e, "last-checkpoint"] => VfsPath::EpochLastCheckpoint(
170 e.parse()
171 .map_err(|_| bad_request(format!("invalid epoch: {e}")))?,
172 ),
173 ["epochs", e, "committee"] => VfsPath::EpochCommittee(
174 e.parse()
175 .map_err(|_| bad_request(format!("invalid epoch: {e}")))?,
176 ),
177 ["epochs", e, "checkpoints"] => VfsPath::EpochCheckpoints(
178 e.parse()
179 .map_err(|_| bad_request(format!("invalid epoch: {e}")))?,
180 ),
181 ["epochs", e, "checkpoints", ref_str] => {
182 let epoch = e
183 .parse()
184 .map_err(|_| bad_request(format!("invalid epoch: {e}")))?;
185 if let Ok(seq) = ref_str.parse::<CheckpointSequenceNumber>() {
186 VfsPath::EpochCheckpointBySeq(epoch, seq)
187 } else {
188 let digest: CheckpointDigest = ref_str
189 .parse()
190 .map_err(|_| bad_request(format!("invalid checkpoint ref: {ref_str}")))?;
191 VfsPath::EpochCheckpointByDigest(epoch, digest)
192 }
193 }
194 ["checkpoints"] => VfsPath::CheckpointsRoot,
195 ["checkpoints", "seq"] => VfsPath::CheckpointsSeqRoot,
196 ["checkpoints", "seq", s] => VfsPath::CheckpointsBySeq(
197 s.parse()
198 .map_err(|_| bad_request(format!("invalid sequence: {s}")))?,
199 ),
200 ["checkpoints", "seq", s, "summary"] => VfsPath::CheckpointSeqSummary(
201 s.parse()
202 .map_err(|_| bad_request(format!("invalid sequence: {s}")))?,
203 ),
204 ["checkpoints", "seq", s, "contents"] => VfsPath::CheckpointSeqContents(
205 s.parse()
206 .map_err(|_| bad_request(format!("invalid sequence: {s}")))?,
207 ),
208 ["checkpoints", "seq", s, "contents-short"] => VfsPath::CheckpointSeqContentsShort(
209 s.parse()
210 .map_err(|_| bad_request(format!("invalid sequence: {s}")))?,
211 ),
212 ["checkpoints", "digest"] => VfsPath::CheckpointsDigestRoot,
213 ["checkpoints", "digest", d] => VfsPath::CheckpointsByDigest(
214 d.parse()
215 .map_err(|_| bad_request(format!("invalid digest: {d}")))?,
216 ),
217 ["checkpoints", "digest", d, "summary"] => VfsPath::CheckpointDigestSummary(
218 d.parse()
219 .map_err(|_| bad_request(format!("invalid digest: {d}")))?,
220 ),
221 ["checkpoints", "digest", d, "contents"] => VfsPath::CheckpointDigestContents(
222 d.parse()
223 .map_err(|_| bad_request(format!("invalid digest: {d}")))?,
224 ),
225 ["checkpoints", "digest", d, "contents-short"] => VfsPath::CheckpointDigestContentsShort(
226 d.parse()
227 .map_err(|_| bad_request(format!("invalid digest: {d}")))?,
228 ),
229 ["checkpoint-contents"] => VfsPath::CheckpointContentsRoot,
230 ["checkpoint-contents", d] => VfsPath::CheckpointContentsEntry(
231 d.parse()
232 .map_err(|_| bad_request(format!("invalid contents digest: {d}")))?,
233 ),
234 ["transactions"] => VfsPath::TransactionsRoot,
235 ["transactions", seg] => parse_transaction_seg(seg)?,
236 ["consensus"] => VfsPath::ConsensusRoot,
237 ["consensus", "latest"] => VfsPath::ConsensusLatest,
238 ["consensus", "commits"] => VfsPath::ConsensusCommitsRoot,
239 ["consensus", "commits", i] => VfsPath::ConsensusCommitDir(
240 i.parse()
241 .map_err(|_| bad_request(format!("invalid commit index: {i}")))?,
242 ),
243 ["consensus", "commits", i, "summary"] => VfsPath::ConsensusCommitSummary(
244 i.parse()
245 .map_err(|_| bad_request(format!("invalid commit index: {i}")))?,
246 ),
247 _ => return Err(bad_request(format!("unknown path: {s}"))),
248 };
249 Ok(r)
250}
251
252fn parse_transaction_seg(seg: &str) -> Result<VfsPath, ApiError> {
253 if let Some((tx_str, fx_str)) = seg.split_once(".fx-") {
254 let tx: TransactionDigest = tx_str
255 .parse()
256 .map_err(|_| bad_request(format!("invalid transaction digest: {tx_str}")))?;
257 let fx: TransactionEffectsDigest = fx_str
258 .parse()
259 .map_err(|_| bad_request(format!("invalid effects digest: {fx_str}")))?;
260 Ok(VfsPath::TransactionEffectsEntry(tx, fx))
261 } else {
262 let tx: TransactionDigest = seg
263 .parse()
264 .map_err(|_| bad_request(format!("invalid transaction digest: {seg}")))?;
265 Ok(VfsPath::TransactionEntry(tx))
266 }
267}
268
269pub(crate) async fn handle_ls(
272 State(state): State<Arc<AppState>>,
273 Query(params): Query<LsParams>,
274) -> Result<Json<Vec<DirEntry>>, ApiError> {
275 let path = parse_path(¶ms.path)?;
276 let limit = params.limit.min(1000);
277 let cp_store = state.node.clone_checkpoint_store();
278 let committee_store = state.node.clone_committee_store();
279 let auth_store = state.node.clone_authority_store();
280 let consensus_store = state.node.clone_consensus_store();
281
282 let entries = match (&path, params.cursor) {
283 (VfsPath::CheckpointsBySeq(seq), true) => {
284 list_checkpoints_from_seq(&cp_store, Some(*seq), limit)?
285 }
286 (VfsPath::CheckpointsByDigest(d), true) => {
287 list_checkpoint_digests_from(&cp_store, Some(*d), limit)?
288 }
289 (VfsPath::EpochCheckpointBySeq(epoch, seq), true) => {
290 list_epoch_checkpoints_from(&cp_store, *epoch, Some(*seq), limit)?
291 }
292 (VfsPath::CheckpointContentsEntry(d), true) => {
293 list_checkpoint_contents_from(&cp_store, Some(*d), limit)?
294 }
295 (VfsPath::TransactionEntry(d), true) => {
296 list_transactions_from(&auth_store, Some(*d), limit)?
297 }
298 (VfsPath::ConsensusCommitDir(idx), true) => {
299 list_consensus_commits_from(consensus_store.as_deref(), Some(*idx), limit)?
300 }
301 _ => list_children(
302 &path,
303 &cp_store,
304 &committee_store,
305 &auth_store,
306 consensus_store.as_deref(),
307 limit,
308 )?,
309 };
310
311 Ok(Json(entries))
312}
313
314fn list_children(
315 path: &VfsPath,
316 cp_store: &sui_core::checkpoints::CheckpointStore,
317 committee_store: &sui_core::epoch::committee_store::CommitteeStore,
318 auth_store: &sui_core::authority::AuthorityStore,
319 consensus_store: Option<&RocksDBStore>,
320 limit: usize,
321) -> Result<Vec<DirEntry>, ApiError> {
322 match path {
323 VfsPath::Root => Ok(vec![
324 DirEntry {
325 name: "epochs".into(),
326 is_dir: true,
327 },
328 DirEntry {
329 name: "checkpoints".into(),
330 is_dir: true,
331 },
332 DirEntry {
333 name: "checkpoint-contents".into(),
334 is_dir: true,
335 },
336 DirEntry {
337 name: "transactions".into(),
338 is_dir: true,
339 },
340 DirEntry {
341 name: "consensus".into(),
342 is_dir: true,
343 },
344 ]),
345 VfsPath::Epochs => {
346 let epochs = committee_store.list_epochs(None, limit).map_err(internal)?;
347 Ok(epochs
348 .into_iter()
349 .map(|(id, _)| DirEntry {
350 name: id.to_string(),
351 is_dir: true,
352 })
353 .collect())
354 }
355 VfsPath::Epoch(_) => Ok(vec![
356 DirEntry {
357 name: "first-checkpoint".into(),
358 is_dir: false,
359 },
360 DirEntry {
361 name: "last-checkpoint".into(),
362 is_dir: false,
363 },
364 DirEntry {
365 name: "committee".into(),
366 is_dir: false,
367 },
368 DirEntry {
369 name: "checkpoints".into(),
370 is_dir: true,
371 },
372 ]),
373 VfsPath::EpochCheckpoints(epoch) => {
374 list_epoch_checkpoints_from(cp_store, *epoch, None, limit)
375 }
376 VfsPath::CheckpointsRoot => Ok(vec![
377 DirEntry {
378 name: "seq".into(),
379 is_dir: true,
380 },
381 DirEntry {
382 name: "digest".into(),
383 is_dir: true,
384 },
385 ]),
386 VfsPath::CheckpointsSeqRoot => list_checkpoints_from_seq(cp_store, None, limit),
387 VfsPath::CheckpointsBySeq(_) => Ok(vec![
388 DirEntry {
389 name: "summary".into(),
390 is_dir: false,
391 },
392 DirEntry {
393 name: "contents".into(),
394 is_dir: false,
395 },
396 DirEntry {
397 name: "contents-short".into(),
398 is_dir: false,
399 },
400 ]),
401 VfsPath::CheckpointsDigestRoot => list_checkpoint_digests_from(cp_store, None, limit),
402 VfsPath::CheckpointsByDigest(_) => Ok(vec![
403 DirEntry {
404 name: "summary".into(),
405 is_dir: false,
406 },
407 DirEntry {
408 name: "contents".into(),
409 is_dir: false,
410 },
411 DirEntry {
412 name: "contents-short".into(),
413 is_dir: false,
414 },
415 ]),
416 VfsPath::CheckpointContentsRoot => list_checkpoint_contents_from(cp_store, None, limit),
417 VfsPath::TransactionsRoot => list_transactions_from(auth_store, None, limit),
418 VfsPath::ConsensusRoot => Ok(vec![
419 DirEntry {
420 name: "latest".into(),
421 is_dir: false,
422 },
423 DirEntry {
424 name: "commits".into(),
425 is_dir: true,
426 },
427 ]),
428 VfsPath::ConsensusCommitsRoot => list_consensus_commits_from(consensus_store, None, limit),
429 VfsPath::ConsensusCommitDir(_) => Ok(vec![DirEntry {
430 name: "summary".into(),
431 is_dir: false,
432 }]),
433 _ => Err(bad_request("path is not a directory")),
434 }
435}
436
437fn list_checkpoints_from_seq(
438 cp_store: &sui_core::checkpoints::CheckpointStore,
439 start: Option<CheckpointSequenceNumber>,
440 limit: usize,
441) -> Result<Vec<DirEntry>, ApiError> {
442 cp_store
443 .list_checkpoints_from_seq(start, limit)
444 .map(|items| {
445 items
446 .into_iter()
447 .map(|(seq, _)| DirEntry {
448 name: seq.to_string(),
449 is_dir: true,
450 })
451 .collect()
452 })
453 .map_err(internal)
454}
455
456fn list_checkpoint_digests_from(
457 cp_store: &sui_core::checkpoints::CheckpointStore,
458 start: Option<CheckpointDigest>,
459 limit: usize,
460) -> Result<Vec<DirEntry>, ApiError> {
461 cp_store
462 .list_checkpoint_digests(start, limit)
463 .map(|items| {
464 items
465 .into_iter()
466 .map(|d| DirEntry {
467 name: d.to_string(),
468 is_dir: true,
469 })
470 .collect()
471 })
472 .map_err(internal)
473}
474
475fn list_checkpoint_contents_from(
476 cp_store: &sui_core::checkpoints::CheckpointStore,
477 start: Option<CheckpointContentsDigest>,
478 limit: usize,
479) -> Result<Vec<DirEntry>, ApiError> {
480 cp_store
481 .list_checkpoint_contents_digests(start, limit)
482 .map(|items| {
483 items
484 .into_iter()
485 .map(|d| DirEntry {
486 name: d.to_string(),
487 is_dir: false,
488 })
489 .collect()
490 })
491 .map_err(internal)
492}
493
494fn list_epoch_checkpoints_from(
495 cp_store: &sui_core::checkpoints::CheckpointStore,
496 epoch: EpochId,
497 start: Option<CheckpointSequenceNumber>,
498 limit: usize,
499) -> Result<Vec<DirEntry>, ApiError> {
500 cp_store
501 .list_epoch_checkpoints(epoch, start, limit)
502 .map(|items| {
503 items
504 .into_iter()
505 .map(|(seq, _)| DirEntry {
506 name: seq.to_string(),
507 is_dir: false,
508 })
509 .collect()
510 })
511 .map_err(internal)
512}
513
514fn list_transactions_from(
515 auth_store: &sui_core::authority::AuthorityStore,
516 start: Option<TransactionDigest>,
517 limit: usize,
518) -> Result<Vec<DirEntry>, ApiError> {
519 let tx_digests = auth_store
520 .list_transactions_from(start, limit)
521 .map_err(internal)?;
522 let mut entries = Vec::with_capacity(tx_digests.len() * 2);
523 for digest in &tx_digests {
524 entries.push(DirEntry {
525 name: digest.to_string(),
526 is_dir: false,
527 });
528 if let Ok(Some(fx_digest)) = auth_store.get_executed_effects_digest_for_tx(digest) {
529 entries.push(DirEntry {
530 name: format!("{digest}.fx-{fx_digest}"),
531 is_dir: false,
532 });
533 }
534 }
535 Ok(entries)
536}
537
538fn list_consensus_commits_from(
539 consensus_store: Option<&RocksDBStore>,
540 start: Option<u32>,
541 limit: usize,
542) -> Result<Vec<DirEntry>, ApiError> {
543 let cs = consensus_store.ok_or_else(|| {
544 not_implemented("consensus store not available (node is not a validator)")
545 })?;
546 let start_idx = start.unwrap_or(0);
547 let end_idx = start_idx.saturating_add(limit as u32);
548 let commits = cs
549 .scan_commits(CommitRange::new(start_idx..=end_idx))
550 .map_err(internal)?;
551 Ok(commits
552 .into_iter()
553 .map(|c| DirEntry {
554 name: c.index().to_string(),
555 is_dir: true,
556 })
557 .collect())
558}
559
560fn render_consensus_commit_summary(
561 consensus_store: Option<&RocksDBStore>,
562 index: u32,
563 format: ReadFormat,
564) -> Result<Response, ApiError> {
565 let cs = consensus_store.ok_or_else(|| {
566 not_implemented("consensus store not available (node is not a validator)")
567 })?;
568 let summary = sui_core::consensus_commit_summary::build_consensus_commit_summary(cs, index)
569 .map_err(internal)?
570 .ok_or_else(|| not_found(format!("consensus commit {index} not found")))?;
571 let commit_index = summary.commit.index();
572
573 match format {
574 ReadFormat::Json => {
575 let val = serde_json::json!({
576 "index": commit_index,
577 "transactions": summary.tx_keys,
578 "missing_blocks": summary.missing_blocks,
579 });
580 Ok(Json(val).into_response())
581 }
582 ReadFormat::Debug | ReadFormat::Bcs | ReadFormat::RawBcs => {
583 let mut text = format!("commit {commit_index}\n");
584 for key in &summary.tx_keys {
585 text.push_str(&format!(" {key}\n"));
586 }
587 for r in &summary.missing_blocks {
588 text.push_str(&format!(" [missing block: {r}]\n"));
589 }
590 Ok(text.into_response())
591 }
592 }
593}
594
595pub(crate) async fn handle_read(
598 State(state): State<Arc<AppState>>,
599 Query(params): Query<ReadParams>,
600) -> Result<Response, ApiError> {
601 let path = parse_path(¶ms.path)?;
602 let cp_store = state.node.clone_checkpoint_store();
603 let committee_store = state.node.clone_committee_store();
604 let auth_store = state.node.clone_authority_store();
605 let consensus_store = state.node.clone_consensus_store();
606 resolve_read(
607 &path,
608 &cp_store,
609 &committee_store,
610 &auth_store,
611 consensus_store.as_deref(),
612 params.format,
613 )
614}
615
616fn resolve_read(
617 path: &VfsPath,
618 cp_store: &sui_core::checkpoints::CheckpointStore,
619 committee_store: &sui_core::epoch::committee_store::CommitteeStore,
620 auth_store: &sui_core::authority::AuthorityStore,
621 consensus_store: Option<&RocksDBStore>,
622 format: ReadFormat,
623) -> Result<Response, ApiError> {
624 match path {
625 VfsPath::EpochFirstCheckpoint(epoch) => {
626 let first_seq = cp_store
627 .get_epoch_first_checkpoint_seq(*epoch)
628 .map_err(internal)?
629 .ok_or_else(|| not_found(format!("no data for epoch {epoch}")))?;
630 let cp = cp_store
631 .get_checkpoint_by_sequence_number(first_seq)
632 .map_err(internal)?
633 .ok_or_else(|| not_found(format!("checkpoint {first_seq} not found")))?;
634 render_summary(cp.data(), format)
635 }
636 VfsPath::EpochLastCheckpoint(epoch) => {
637 let cp = cp_store
638 .get_epoch_last_checkpoint(*epoch)
639 .map_err(internal)?
640 .ok_or_else(|| not_found(format!("no last checkpoint for epoch {epoch}")))?;
641 render_summary(cp.data(), format)
642 }
643 VfsPath::EpochCommittee(epoch) => {
644 let committee = committee_store
645 .get_committee(epoch)
646 .map_err(internal)?
647 .ok_or_else(|| not_found(format!("no committee for epoch {epoch}")))?;
648 render_value(committee.as_ref(), format)
649 }
650 VfsPath::EpochCheckpointBySeq(_epoch, seq) => {
651 let cp = cp_store
652 .get_checkpoint_by_sequence_number(*seq)
653 .map_err(internal)?
654 .ok_or_else(|| not_found(format!("checkpoint {seq} not found")))?;
655 render_summary(cp.data(), format)
656 }
657 VfsPath::EpochCheckpointByDigest(_epoch, digest) => {
658 let cp = cp_store
659 .get_checkpoint_by_digest(digest)
660 .map_err(internal)?
661 .ok_or_else(|| not_found(format!("checkpoint {digest} not found")))?;
662 render_summary(cp.data(), format)
663 }
664 VfsPath::CheckpointSeqSummary(seq) => {
665 let cp = cp_store
666 .get_checkpoint_by_sequence_number(*seq)
667 .map_err(internal)?
668 .ok_or_else(|| not_found(format!("checkpoint {seq} not found")))?;
669 render_summary(cp.data(), format)
670 }
671 VfsPath::CheckpointSeqContents(seq) => {
672 let cp = cp_store
673 .get_checkpoint_by_sequence_number(*seq)
674 .map_err(internal)?
675 .ok_or_else(|| not_found(format!("checkpoint {seq} not found")))?;
676 let contents = cp_store
677 .get_checkpoint_contents(&cp.content_digest)
678 .map_err(internal)?
679 .ok_or_else(|| not_found(format!("contents for checkpoint {seq} not found")))?;
680 render_value(&contents, format)
681 }
682 VfsPath::CheckpointSeqContentsShort(seq) => {
683 let cp = cp_store
684 .get_checkpoint_by_sequence_number(*seq)
685 .map_err(internal)?
686 .ok_or_else(|| not_found(format!("checkpoint {seq} not found")))?;
687 let contents = cp_store
688 .get_checkpoint_contents(&cp.content_digest)
689 .map_err(internal)?
690 .ok_or_else(|| not_found(format!("contents for checkpoint {seq} not found")))?;
691 render_contents_short(&contents, format)
692 }
693 VfsPath::CheckpointDigestSummary(digest) => {
694 let cp = cp_store
695 .get_checkpoint_by_digest(digest)
696 .map_err(internal)?
697 .ok_or_else(|| not_found(format!("checkpoint {digest} not found")))?;
698 render_summary(cp.data(), format)
699 }
700 VfsPath::CheckpointDigestContents(digest) => {
701 let cp = cp_store
702 .get_checkpoint_by_digest(digest)
703 .map_err(internal)?
704 .ok_or_else(|| not_found(format!("checkpoint {digest} not found")))?;
705 let contents = cp_store
706 .get_checkpoint_contents(&cp.content_digest)
707 .map_err(internal)?
708 .ok_or_else(|| not_found(format!("contents for checkpoint {digest} not found")))?;
709 render_value(&contents, format)
710 }
711 VfsPath::CheckpointDigestContentsShort(digest) => {
712 let cp = cp_store
713 .get_checkpoint_by_digest(digest)
714 .map_err(internal)?
715 .ok_or_else(|| not_found(format!("checkpoint {digest} not found")))?;
716 let contents = cp_store
717 .get_checkpoint_contents(&cp.content_digest)
718 .map_err(internal)?
719 .ok_or_else(|| not_found(format!("contents for checkpoint {digest} not found")))?;
720 render_contents_short(&contents, format)
721 }
722 VfsPath::CheckpointContentsEntry(digest) => {
723 let contents = cp_store
724 .get_checkpoint_contents(digest)
725 .map_err(internal)?
726 .ok_or_else(|| not_found(format!("checkpoint contents {digest} not found")))?;
727 render_value(&contents, format)
728 }
729 VfsPath::TransactionEntry(digest) => {
730 let tx = auth_store
731 .get_transaction_block(digest)
732 .map_err(internal)?
733 .ok_or_else(|| not_found(format!("transaction {digest} not found")))?;
734 render_value(tx.data(), format)
735 }
736 VfsPath::TransactionEffectsEntry(tx_digest, fx_digest) => {
737 let effects = auth_store
738 .get_effects(fx_digest)
739 .map_err(internal)?
740 .ok_or_else(|| {
741 not_found(format!("effects {fx_digest} for tx {tx_digest} not found"))
742 })?;
743 render_value(&effects, format)
744 }
745 VfsPath::ConsensusLatest => {
746 let cs = consensus_store.ok_or_else(|| {
747 not_implemented("consensus store not available (node is not a validator)")
748 })?;
749 let commit = cs
750 .read_last_commit()
751 .map_err(internal)?
752 .ok_or_else(|| not_found("no commits yet"))?;
753 let index = commit.index();
754 match format {
755 ReadFormat::Json => Ok(Json(serde_json::json!({ "index": index })).into_response()),
756 _ => Ok(index.to_string().into_response()),
757 }
758 }
759 VfsPath::ConsensusCommitSummary(index) => {
760 render_consensus_commit_summary(consensus_store, *index, format)
761 }
762 VfsPath::Epoch(epoch) => Err(bad_request(format!("epoch {epoch} is a directory"))),
763 _ => Err(bad_request("path is not a readable file")),
764 }
765}
766
767fn render_contents_short(
768 contents: &sui_types::messages_checkpoint::CheckpointContents,
769 format: ReadFormat,
770) -> Result<Response, ApiError> {
771 match format {
772 ReadFormat::Json => {
773 let pairs: Vec<JsonValue> = contents
774 .iter()
775 .map(|ed| {
776 serde_json::json!({
777 "transaction": ed.transaction.to_string(),
778 "effects": ed.effects.to_string(),
779 })
780 })
781 .collect();
782 Ok(Json(JsonValue::Array(pairs)).into_response())
783 }
784 ReadFormat::Debug => {
785 let mut text = String::new();
786 for ed in contents.iter() {
787 text.push_str(&format!("{} {}\n", ed.transaction, ed.effects));
788 }
789 Ok(text.into_response())
790 }
791 ReadFormat::Bcs | ReadFormat::RawBcs => Err(bad_request(
792 "bcs not supported for contents-short; use 'contents' instead",
793 )),
794 }
795}
796
797fn render_summary<T>(value: &T, format: ReadFormat) -> Result<Response, ApiError>
798where
799 T: serde::Serialize + std::fmt::Debug,
800{
801 render_value(value, format)
802}
803
804fn render_value<T>(value: &T, format: ReadFormat) -> Result<Response, ApiError>
805where
806 T: serde::Serialize + std::fmt::Debug,
807{
808 match format {
809 ReadFormat::Json => {
810 let v: JsonValue = serde_json::to_value(value)
811 .map_err(|e| internal(format!("serialize error: {e}")))?;
812 Ok(Json(v).into_response())
813 }
814 ReadFormat::Debug => Ok(format!("{value:#?}").into_response()),
815 ReadFormat::Bcs => {
816 let bytes = bcs::to_bytes(value).map_err(|e| internal(format!("bcs error: {e}")))?;
817 Ok(base64::engine::general_purpose::STANDARD
818 .encode(&bytes)
819 .into_response())
820 }
821 ReadFormat::RawBcs => {
822 let bytes = bcs::to_bytes(value).map_err(|e| internal(format!("bcs error: {e}")))?;
823 let mut headers = HeaderMap::new();
824 headers.insert(
825 axum::http::header::CONTENT_TYPE,
826 HeaderValue::from_static("application/octet-stream"),
827 );
828 Ok((headers, bytes).into_response())
829 }
830 }
831}
832
833pub(crate) async fn handle_delete(
836 State(_state): State<Arc<AppState>>,
837 Query(params): Query<DeleteParams>,
838) -> impl IntoResponse {
839 (
840 StatusCode::NOT_IMPLEMENTED,
841 format!("delete not yet implemented for path: {}", params.path),
842 )
843}