mithril_client_cli/utils/
cardano_db.rs

1use anyhow::anyhow;
2use futures::Future;
3use indicatif::{MultiProgress, ProgressBar};
4use std::path::Path;
5use std::time::Duration;
6
7use super::CardanoDbDownloadCheckerError;
8use mithril_client::{MithrilError, MithrilResult};
9
10/// Utility functions for to the CardanoDb commands
11pub struct CardanoDbUtils;
12
13/// Known formats of a ledger state snapshot
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum LedgerFormat {
16    /// Legacy in-memory format (maximum supported by cardano-node `10.3.0`)
17    Legacy,
18    /// UTxO-HD in-memory format (since cardano-node `10.4.1`)
19    InMemory,
20    /// UTxO-HD Lmdb format (since cardano-node `10.4.1`)
21    Lmdb,
22}
23
24impl CardanoDbUtils {
25    /// Handle the error return by `check_prerequisites`
26    pub fn check_disk_space_error(error: MithrilError) -> MithrilResult<String> {
27        match error.downcast_ref::<CardanoDbDownloadCheckerError>() {
28            Some(CardanoDbDownloadCheckerError::NotEnoughSpaceForArchive { .. })
29            | Some(CardanoDbDownloadCheckerError::NotEnoughSpaceForUncompressedData { .. }) => {
30                Ok(format!("Warning: {error}"))
31            }
32            _ => Err(error),
33        }
34    }
35
36    /// Display a spinner while waiting for the result of a future
37    pub async fn wait_spinner<T, E>(
38        progress_bar: &MultiProgress,
39        future: impl Future<Output = Result<T, E>>,
40    ) -> MithrilResult<T>
41    where
42        MithrilError: From<E>,
43    {
44        let pb = progress_bar.add(ProgressBar::new_spinner());
45        let spinner = async move {
46            loop {
47                pb.tick();
48                tokio::time::sleep(Duration::from_millis(50)).await;
49            }
50        };
51
52        tokio::select! {
53            _ = spinner => Err(anyhow!("timeout")),
54            res = future => res.map_err(Into::into),
55        }
56    }
57
58    pub fn format_bytes_to_gigabytes(bytes: u64) -> String {
59        let size_in_giga = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
60
61        format!("{size_in_giga:.2} GiB")
62    }
63
64    /// Returns the docker run command to run cardano-node with the given ledger format.
65    ///
66    /// If the ledger format is [legacy](LedgerFormat::Legacy), the cardano node version will
67    /// be forced to `10.3.1`.
68    pub fn get_docker_run_command<P: AsRef<Path>>(
69        canonical_db_filepath: P,
70        cardano_network: &str,
71        cardano_node_version: &str,
72        ledger_format: LedgerFormat,
73    ) -> String {
74        let db_path = canonical_db_filepath.as_ref();
75        let cardano_node_version = if matches!(ledger_format, LedgerFormat::Legacy) {
76            "10.3.1"
77        } else {
78            cardano_node_version
79        };
80        let cardano_node_config = match ledger_format {
81            LedgerFormat::Lmdb => {
82                r#" -e CARDANO_CONFIG_JSON_MERGE='{"LedgerDB": { "Backend": "V1LMDB" }}'"#
83            }
84            _ => "",
85        };
86
87        let docker_cmd = format!(
88            "docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source=\"{db_path}\",target=/data/db/ -e NETWORK={cardano_network}{cardano_node_config} ghcr.io/intersectmbo/cardano-node:{cardano_node_version}",
89            db_path = db_path.display(),
90        );
91
92        docker_cmd
93    }
94}
95
96#[cfg(test)]
97mod test {
98    use super::*;
99    use std::path::PathBuf;
100
101    #[test]
102    fn check_disk_space_error_should_return_warning_message_if_error_is_not_enough_space_for_archive()
103     {
104        let not_enough_space_error = CardanoDbDownloadCheckerError::NotEnoughSpaceForArchive {
105            left_space: 1_f64,
106            pathdir: PathBuf::new(),
107            archive_size: 2_f64,
108        };
109        let expected = format!("Warning: {not_enough_space_error}");
110
111        let result = CardanoDbUtils::check_disk_space_error(anyhow!(not_enough_space_error))
112            .expect("check_disk_space_error should not error");
113
114        assert!(result.contains(&expected));
115    }
116
117    #[test]
118    fn check_disk_space_error_should_return_warning_message_if_error_is_not_enough_space_for_uncompressed_data()
119     {
120        let not_enough_space_error =
121            CardanoDbDownloadCheckerError::NotEnoughSpaceForUncompressedData {
122                left_space: 1_f64,
123                pathdir: PathBuf::new(),
124                db_size: 2_f64,
125            };
126        let expected = format!("Warning: {not_enough_space_error}");
127
128        let result = CardanoDbUtils::check_disk_space_error(anyhow!(not_enough_space_error))
129            .expect("check_disk_space_error should not error");
130
131        assert!(result.contains(&expected));
132    }
133
134    #[test]
135    fn check_disk_space_error_should_return_error_if_error_is_not_error_not_enough_space() {
136        let error = CardanoDbDownloadCheckerError::UnpackDirectoryNotEmpty(PathBuf::new());
137
138        let error = CardanoDbUtils::check_disk_space_error(anyhow!(error))
139            .expect_err("check_disk_space_error should fail");
140
141        assert!(
142            matches!(
143                error.downcast_ref::<CardanoDbDownloadCheckerError>(),
144                Some(CardanoDbDownloadCheckerError::UnpackDirectoryNotEmpty(_))
145            ),
146            "Unexpected error: {error:?}"
147        );
148    }
149
150    #[test]
151    fn format_bytes_to_gigabytes_zero() {
152        let one_gigabyte = 1024 * 1024 * 1024;
153
154        assert_eq!(CardanoDbUtils::format_bytes_to_gigabytes(0), "0.00 GiB");
155
156        assert_eq!(
157            CardanoDbUtils::format_bytes_to_gigabytes(one_gigabyte),
158            "1.00 GiB"
159        );
160
161        assert_eq!(
162            CardanoDbUtils::format_bytes_to_gigabytes(one_gigabyte / 2),
163            "0.50 GiB"
164        );
165
166        assert_eq!(
167            CardanoDbUtils::format_bytes_to_gigabytes(one_gigabyte * 10),
168            "10.00 GiB"
169        );
170    }
171
172    #[test]
173    fn get_docker_run_command_for_legacy_ledger() {
174        let run_command = CardanoDbUtils::get_docker_run_command(
175            Path::new("/path/to/db"),
176            "mainnet",
177            "whatever",
178            LedgerFormat::Legacy,
179        );
180
181        assert_eq!(
182            run_command,
183            r#"docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="/path/to/db",target=/data/db/ -e NETWORK=mainnet ghcr.io/intersectmbo/cardano-node:10.3.1"#
184        )
185    }
186
187    #[test]
188    fn get_docker_run_command_for_in_memory_ledger() {
189        let run_command = CardanoDbUtils::get_docker_run_command(
190            Path::new("/path/to/db"),
191            "mainnet",
192            "10.5.4",
193            LedgerFormat::InMemory,
194        );
195
196        assert_eq!(
197            run_command,
198            r#"docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="/path/to/db",target=/data/db/ -e NETWORK=mainnet ghcr.io/intersectmbo/cardano-node:10.5.4"#
199        )
200    }
201
202    #[test]
203    fn get_docker_run_command_for_lmdb_ledger() {
204        let run_command = CardanoDbUtils::get_docker_run_command(
205            Path::new("/path/to/db"),
206            "mainnet",
207            "10.6.2",
208            LedgerFormat::Lmdb,
209        );
210
211        assert_eq!(
212            run_command,
213            r#"docker run -v cardano-node-ipc:/ipc -v cardano-node-data:/data --mount type=bind,source="/path/to/db",target=/data/db/ -e NETWORK=mainnet -e CARDANO_CONFIG_JSON_MERGE='{"LedgerDB": { "Backend": "V1LMDB" }}' ghcr.io/intersectmbo/cardano-node:10.6.2"#
214        )
215    }
216}