mithril_client_cli/commands/cardano_stake_distribution/
download.rs1use anyhow::{Context, anyhow};
2use clap::Parser;
3use std::sync::Arc;
4use std::{
5 collections::HashMap,
6 path::{Path, PathBuf},
7};
8
9use crate::utils::{
10 self, ExpanderUtils, IndicatifFeedbackReceiver, ProgressOutputType, ProgressPrinter,
11};
12use crate::{
13 CommandContext,
14 commands::client_builder,
15 configuration::{ConfigError, ConfigSource},
16};
17use mithril_client::Client;
18use mithril_client::common::Epoch;
19use mithril_client::{CardanoStakeDistribution, MessageBuilder, MithrilResult};
20
21#[derive(Parser, Debug, Clone)]
23pub struct CardanoStakeDistributionDownloadCommand {
24 unique_identifier: String,
28
29 #[clap(long)]
31 download_dir: Option<PathBuf>,
32
33 #[clap(long, env = "GENESIS_VERIFICATION_KEY")]
35 genesis_verification_key: Option<String>,
36}
37
38impl CardanoStakeDistributionDownloadCommand {
39 pub async fn execute(&self, mut context: CommandContext) -> MithrilResult<()> {
41 context.config_parameters_mut().add_source(self)?;
42 let download_dir = context.config_parameters().get_or("download_dir", ".");
43 let download_dir = Path::new(&download_dir);
44 let logger = context.logger();
45
46 let progress_output_type = if context.is_json_output_enabled() {
47 ProgressOutputType::JsonReporter
48 } else {
49 ProgressOutputType::Tty
50 };
51 let progress_printer = ProgressPrinter::new(progress_output_type, 4);
52 let client = client_builder(context.config_parameters())?
53 .add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(
54 progress_output_type,
55 logger.clone(),
56 )))
57 .with_logger(logger.clone())
58 .build()?;
59
60 progress_printer.report_step(
61 1,
62 &format!(
63 "Fetching Cardano stake distribution for identifier: '{}' …",
64 self.unique_identifier
65 ),
66 )?;
67 let cardano_stake_distribution =
68 Self::fetch_cardano_stake_distribution_from_unique_identifier(
69 &client,
70 &self.unique_identifier,
71 )
72 .await
73 .with_context(|| {
74 format!(
75 "Can not fetch Cardano stake distribution from unique identifier: '{}'",
76 &self.unique_identifier
77 )
78 })?;
79
80 progress_printer.report_step(
81 2,
82 "Fetching the certificate and verifying the certificate chain…",
83 )?;
84 let certificate = client
85 .certificate()
86 .verify_chain(&cardano_stake_distribution.certificate_hash)
87 .await
88 .with_context(|| {
89 format!(
90 "Can not verify the certificate chain from certificate_hash: '{}'",
91 &cardano_stake_distribution.certificate_hash
92 )
93 })?;
94
95 progress_printer.report_step(
96 3,
97 "Verify that the Cardano stake distribution is signed in the associated certificate",
98 )?;
99 let message = MessageBuilder::new()
100 .compute_cardano_stake_distribution_message(&certificate, &cardano_stake_distribution)
101 .with_context(
102 || "Can not compute the message for the given Cardano stake distribution",
103 )?;
104
105 if !certificate.match_message(&message) {
106 return Err(anyhow!(
107 "Certificate and message did not match:\ncertificate_message: '{}'\n computed_message: '{}'",
108 certificate.signed_message,
109 message.compute_hash()
110 ));
111 }
112
113 progress_printer.report_step(4, "Writing fetched Cardano stake distribution to a file")?;
114 if !download_dir.is_dir() {
115 std::fs::create_dir_all(download_dir)?;
116 }
117 let filepath = PathBuf::new().join(download_dir).join(format!(
118 "cardano_stake_distribution-{}.json",
119 cardano_stake_distribution.epoch
120 ));
121 std::fs::write(
122 &filepath,
123 serde_json::to_string(&cardano_stake_distribution).with_context(|| {
124 format!(
125 "Can not serialize Cardano stake distribution artifact '{cardano_stake_distribution:?}'"
126 )
127 })?,
128 )?;
129
130 if context.is_json_output_enabled() {
131 println!(
132 r#"{{"cardano_stake_distribution_epoch": "{}", "filepath": "{}"}}"#,
133 cardano_stake_distribution.epoch,
134 filepath.display()
135 );
136 } else {
137 println!(
138 "Cardano stake distribution for epoch '{}' has been verified and saved as '{}'.",
139 cardano_stake_distribution.epoch,
140 filepath.display()
141 );
142 }
143
144 Ok(())
145 }
146
147 fn is_sha256_hash(identifier: &str) -> bool {
148 identifier.len() == 64 && identifier.chars().all(|c| c.is_ascii_hexdigit())
149 }
150
151 async fn fetch_cardano_stake_distribution_from_unique_identifier(
153 client: &Client,
154 unique_identifier: &str,
155 ) -> MithrilResult<CardanoStakeDistribution> {
156 if Self::is_sha256_hash(unique_identifier) {
157 client
158 .cardano_stake_distribution()
159 .get(unique_identifier)
160 .await
161 .with_context(|| {
162 format!(
163 "Can not download and verify the artifact for hash: '{unique_identifier}'"
164 )
165 })?
166 .ok_or(anyhow!(
167 "No Cardano stake distribution could be found for hash: '{}'",
168 unique_identifier
169 ))
170 } else {
171 let epoch = {
172 let get_list_of_artifact_epochs = || async {
173 let cardano_stake_distributions = client.cardano_stake_distribution().list().await.with_context(|| {
174 "Can not get the list of artifacts while retrieving the latest Cardano stake distribution epoch"
175 })?;
176
177 Ok(cardano_stake_distributions
178 .iter()
179 .map(|csd| csd.epoch.to_string())
180 .collect::<Vec<String>>())
181 };
182
183 let epoch = ExpanderUtils::expand_eventual_id_alias(
184 unique_identifier,
185 get_list_of_artifact_epochs(),
186 )
187 .await?;
188
189 Epoch(
190 epoch.parse().with_context(|| {
191 format!("Can not convert: '{epoch}' into a valid Epoch")
192 })?,
193 )
194 };
195
196 client
197 .cardano_stake_distribution()
198 .get_by_epoch(epoch)
199 .await
200 .with_context(|| {
201 format!("Can not download and verify the artifact for epoch: '{epoch}'")
202 })?
203 .ok_or(anyhow!(
204 "No Cardano stake distribution could be found for epoch: '{}'",
205 epoch
206 ))
207 }
208 }
209}
210
211impl ConfigSource for CardanoStakeDistributionDownloadCommand {
212 fn collect(&self) -> Result<HashMap<String, String>, ConfigError> {
213 let mut map = HashMap::new();
214
215 if let Some(download_dir) = self.download_dir.clone() {
216 let param = "download_dir".to_string();
217 map.insert(
218 param.clone(),
219 utils::path_to_string(&download_dir)
220 .map_err(|e| ConfigError::Conversion(param, e))?,
221 );
222 }
223
224 if let Some(genesis_verification_key) = self.genesis_verification_key.clone() {
225 map.insert(
226 "genesis_verification_key".to_string(),
227 genesis_verification_key,
228 );
229 }
230
231 Ok(map)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn is_sha_256_returns_false_with_len_different_than_64_and_hex_digit() {
241 let len_65_hex_digit = "65aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
242 assert!(!CardanoStakeDistributionDownloadCommand::is_sha256_hash(
243 len_65_hex_digit
244 ));
245
246 let len_63_hex_digit = "63aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
247 assert!(!CardanoStakeDistributionDownloadCommand::is_sha256_hash(
248 len_63_hex_digit
249 ));
250 }
251
252 #[test]
253 fn is_sha_256_returns_false_with_len_equal_to_64_and_not_hex_digit() {
254 let len_64_not_hex_digit =
255 "64zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";
256 assert!(!CardanoStakeDistributionDownloadCommand::is_sha256_hash(
257 len_64_not_hex_digit
258 ));
259 }
260
261 #[test]
262 fn is_sha_256_returns_true_with_len_equal_to_64_and_hex_digit() {
263 let len_64_hex_digit = "64aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
264 assert!(CardanoStakeDistributionDownloadCommand::is_sha256_hash(
265 len_64_hex_digit
266 ));
267 }
268}