Skip to main content

sui_node/
db_shell.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! REST API handlers for the db-shell command's proxy mode.
5//!
6//! Example curl usage:
7//!   curl 'http://127.0.0.1:1337/db-shell/ls?path=/checkpoints/seq&limit=10'
8//!   curl 'http://127.0.0.1:1337/db-shell/read?path=/checkpoints/seq/1234/summary&format=json'
9//!   curl 'http://127.0.0.1:1337/db-shell/read?path=/checkpoints/seq/1234/summary&format=debug'
10//!   curl 'http://127.0.0.1:1337/db-shell/read?path=/checkpoints/seq/1234/summary&format=bcs'
11//!   curl -X DELETE 'http://127.0.0.1:1337/db-shell/delete?path=/checkpoints/seq/1234/summary'
12
13use 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    /// When true, treat the final path component as a start cursor.
44    #[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
83// ─── unified error type ───────────────────────────────────────────────────────
84
85pub(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// ─── path parsing ─────────────────────────────────────────────────────────────
116
117#[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
269// ─── ls handler ───────────────────────────────────────────────────────────────
270
271pub(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(&params.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
595// ─── read handler ─────────────────────────────────────────────────────────────
596
597pub(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(&params.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
833// ─── delete handler ───────────────────────────────────────────────────────────
834
835pub(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}