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