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