mithril_common/chain_observer/
cli_observer.rs

1use anyhow::{anyhow, Context};
2use async_trait::async_trait;
3use hex::FromHex;
4use nom::IResult;
5use rand_core::RngCore;
6use serde_json::Value;
7use std::collections::HashMap;
8use std::fs;
9use std::path::PathBuf;
10use tokio::process::Command;
11
12use crate::chain_observer::interface::{ChainObserver, ChainObserverError};
13use crate::chain_observer::{ChainAddress, TxDatum};
14use crate::crypto_helper::{encode_bech32, KESPeriod, OpCert, SerDeShelleyFileFormat};
15use crate::entities::{BlockNumber, ChainPoint, Epoch, SlotNumber, StakeDistribution};
16use crate::{CardanoNetwork, StdResult};
17
18const CARDANO_ERA: &str = "latest";
19
20/// `CliRunner` trait defines the asynchronous methods
21/// for interaction with the Cardano CLI.
22#[async_trait]
23pub trait CliRunner {
24    /// Launches a UTxO.
25    async fn launch_utxo(&self, address: &str) -> StdResult<String>;
26    /// Launches the stake distribution.
27    async fn launch_stake_distribution(&self) -> StdResult<String>;
28    /// Launches the stake snapshot.
29    async fn launch_stake_snapshot(&self, stake_pool_id: &str) -> StdResult<String>;
30    /// Launches the stake snapshot for all pools.
31    async fn launch_stake_snapshot_all_pools(&self) -> StdResult<String>;
32    /// Launches the era info.
33    async fn launch_era(&self) -> StdResult<String>;
34    /// Launches the epoch info.
35    async fn launch_epoch(&self) -> StdResult<String>;
36    /// Launches the chain point.
37    async fn launch_chain_point(&self) -> StdResult<String>;
38    /// Launches the kes period.
39    async fn launch_kes_period(&self, opcert_file: &str) -> StdResult<String>;
40}
41
42/// A runner able to request data from a Cardano node using the
43/// [Cardano Cli](https://docs.cardano.org/getting-started/use-cli).
44#[derive(Clone, Debug)]
45pub struct CardanoCliRunner {
46    cli_path: PathBuf,
47    socket_path: PathBuf,
48    network: CardanoNetwork,
49}
50
51impl CardanoCliRunner {
52    /// CardanoCliRunner factory
53    pub fn new(cli_path: PathBuf, socket_path: PathBuf, network: CardanoNetwork) -> Self {
54        Self {
55            cli_path,
56            socket_path,
57            network,
58        }
59    }
60
61    fn random_out_file() -> StdResult<PathBuf> {
62        let mut rng = rand_core::OsRng;
63        let dir = std::env::temp_dir().join("cardano-cli-runner");
64        if !dir.exists() {
65            fs::create_dir_all(&dir)?;
66        }
67        Ok(dir.join(format!("{}.out", rng.next_u64())))
68    }
69
70    fn command_for_utxo(&self, address: &str, out_file: PathBuf) -> Command {
71        let mut command = self.get_command();
72        command
73            .arg(CARDANO_ERA)
74            .arg("query")
75            .arg("utxo")
76            .arg("--address")
77            .arg(address)
78            .arg("--out-file")
79            .arg(out_file);
80        self.post_config_command(&mut command);
81
82        command
83    }
84
85    fn command_for_stake_distribution(&self) -> Command {
86        let mut command = self.get_command();
87        command
88            .arg(CARDANO_ERA)
89            .arg("query")
90            .arg("stake-distribution");
91        self.post_config_command(&mut command);
92
93        command
94    }
95
96    fn command_for_stake_snapshot(&self, stake_pool_id: &str) -> Command {
97        let mut command = self.get_command();
98        command
99            .arg(CARDANO_ERA)
100            .arg("query")
101            .arg("stake-snapshot")
102            .arg("--stake-pool-id")
103            .arg(stake_pool_id);
104        self.post_config_command(&mut command);
105
106        command
107    }
108
109    fn command_for_stake_snapshot_all_pools(&self) -> Command {
110        let mut command = self.get_command();
111        command
112            .arg(CARDANO_ERA)
113            .arg("query")
114            .arg("stake-snapshot")
115            .arg("--all-stake-pools");
116        self.post_config_command(&mut command);
117
118        command
119    }
120
121    fn command_for_era(&self) -> Command {
122        let mut command = self.get_command();
123        command.arg(CARDANO_ERA).arg("query").arg("tip");
124        self.post_config_command(&mut command);
125
126        command
127    }
128
129    fn command_for_epoch(&self) -> Command {
130        let mut command = self.get_command();
131        command.arg(CARDANO_ERA).arg("query").arg("tip");
132        self.post_config_command(&mut command);
133
134        command
135    }
136
137    fn command_for_chain_point(&self) -> Command {
138        let mut command = self.get_command();
139        command.arg(CARDANO_ERA).arg("query").arg("tip");
140        self.post_config_command(&mut command);
141
142        command
143    }
144
145    fn command_for_kes_period(&self, opcert_file: &str) -> Command {
146        let mut command = self.get_command();
147        command
148            .arg(CARDANO_ERA)
149            .arg("query")
150            .arg("kes-period-info")
151            .arg("--op-cert-file")
152            .arg(opcert_file);
153        self.post_config_command(&mut command);
154
155        command
156    }
157
158    fn get_command(&self) -> Command {
159        let mut command = Command::new(&self.cli_path);
160        command.env(
161            "CARDANO_NODE_SOCKET_PATH",
162            self.socket_path.to_string_lossy().as_ref(),
163        );
164
165        command
166    }
167
168    fn post_config_command<'a>(&'a self, command: &'a mut Command) -> &'a mut Command {
169        match self.network {
170            CardanoNetwork::MainNet => command.arg("--mainnet"),
171            CardanoNetwork::DevNet(magic) => command.args(vec![
172                "--cardano-mode",
173                "--testnet-magic",
174                &magic.to_string(),
175            ]),
176            CardanoNetwork::TestNet(magic) => {
177                command.args(vec!["--testnet-magic", &magic.to_string()])
178            }
179        }
180    }
181}
182
183#[async_trait]
184impl CliRunner for CardanoCliRunner {
185    async fn launch_utxo(&self, address: &str) -> StdResult<String> {
186        let out_file = Self::random_out_file()?;
187        let output = self
188            .command_for_utxo(address, out_file.clone())
189            .output()
190            .await?;
191
192        if output.status.success() {
193            Ok(fs::read_to_string(out_file)?.trim().to_string())
194        } else {
195            let message = String::from_utf8_lossy(&output.stderr);
196
197            Err(anyhow!(
198                "Error launching command {:?}, error = '{}'",
199                self.command_for_utxo(address, out_file),
200                message
201            ))
202        }
203    }
204
205    async fn launch_stake_distribution(&self) -> StdResult<String> {
206        let output = self.command_for_stake_distribution().output().await?;
207
208        if output.status.success() {
209            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
210        } else {
211            let message = String::from_utf8_lossy(&output.stderr);
212
213            Err(anyhow!(
214                "Error launching command {:?}, error = '{}'",
215                self.command_for_stake_distribution(),
216                message
217            ))
218        }
219    }
220
221    async fn launch_stake_snapshot(&self, stake_pool_id: &str) -> StdResult<String> {
222        let output = self
223            .command_for_stake_snapshot(stake_pool_id)
224            .output()
225            .await?;
226
227        if output.status.success() {
228            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
229        } else {
230            let message = String::from_utf8_lossy(&output.stderr);
231
232            Err(anyhow!(
233                "Error launching command {:?}, error = '{}'",
234                self.command_for_stake_snapshot(stake_pool_id),
235                message
236            ))
237        }
238    }
239
240    async fn launch_stake_snapshot_all_pools(&self) -> StdResult<String> {
241        let output = self.command_for_stake_snapshot_all_pools().output().await?;
242
243        if output.status.success() {
244            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
245        } else {
246            let message = String::from_utf8_lossy(&output.stderr);
247
248            Err(anyhow!(
249                "Error launching command {:?}, error = '{}'",
250                self.command_for_stake_snapshot_all_pools(),
251                message
252            ))
253        }
254    }
255
256    async fn launch_era(&self) -> StdResult<String> {
257        let output = self.command_for_era().output().await?;
258
259        if output.status.success() {
260            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
261        } else {
262            let message = String::from_utf8_lossy(&output.stderr);
263
264            Err(anyhow!(
265                "Error launching command {:?}, error = '{}'",
266                self.command_for_era(),
267                message
268            ))
269        }
270    }
271
272    async fn launch_epoch(&self) -> StdResult<String> {
273        let output = self.command_for_epoch().output().await?;
274
275        if output.status.success() {
276            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
277        } else {
278            let message = String::from_utf8_lossy(&output.stderr);
279
280            Err(anyhow!(
281                "Error launching command {:?}, error = '{}'",
282                self.command_for_epoch(),
283                message
284            ))
285        }
286    }
287
288    async fn launch_chain_point(&self) -> StdResult<String> {
289        let output = self.command_for_chain_point().output().await?;
290
291        if output.status.success() {
292            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
293        } else {
294            let message = String::from_utf8_lossy(&output.stderr);
295
296            Err(anyhow!(
297                "Error launching command {:?}, error = '{}'",
298                self.command_for_chain_point(),
299                message
300            ))
301        }
302    }
303
304    async fn launch_kes_period(&self, opcert_file: &str) -> StdResult<String> {
305        let output = self.command_for_kes_period(opcert_file).output().await?;
306
307        if output.status.success() {
308            Ok(std::str::from_utf8(&output.stdout)?.trim().to_string())
309        } else {
310            let message = String::from_utf8_lossy(&output.stderr);
311
312            Err(anyhow!(
313                "Error launching command {:?}, error = '{}'",
314                self.command_for_kes_period(opcert_file),
315                message
316            ))
317        }
318    }
319}
320
321/// A [ChainObserver] pulling it's data using a [CardanoCliRunner].
322pub struct CardanoCliChainObserver {
323    cli_runner: Box<dyn CliRunner + Send + Sync>,
324}
325
326impl CardanoCliChainObserver {
327    /// CardanoCliChainObserver factory
328    pub fn new(cli_runner: Box<dyn CliRunner + Send + Sync>) -> Self {
329        Self { cli_runner }
330    }
331
332    // This is the only way I found to tell the compiler the correct types
333    // and lifetimes for the function `double`.
334    fn parse_string<'a>(&'a self, string: &'a str) -> IResult<&'a str, f64> {
335        nom::number::complete::double(string)
336    }
337
338    async fn get_current_stake_value(
339        &self,
340        stake_pool_id: &str,
341    ) -> Result<u64, ChainObserverError> {
342        let stake_pool_snapshot_output = self
343            .cli_runner
344            .launch_stake_snapshot(stake_pool_id)
345            .await
346            .map_err(ChainObserverError::General)?;
347        let stake_pool_snapshot: Value = serde_json::from_str(&stake_pool_snapshot_output)
348            .with_context(|| format!("output was = '{stake_pool_snapshot_output}'"))
349            .map_err(ChainObserverError::InvalidContent)?;
350        if let Value::Number(stake_pool_stake) = &stake_pool_snapshot["poolStakeMark"] {
351            return stake_pool_stake.as_u64().ok_or_else(|| {
352                ChainObserverError::InvalidContent(anyhow!(
353                    "Error: could not parse stake pool value as u64 {stake_pool_stake:?}"
354                ))
355            });
356        }
357        Err(ChainObserverError::InvalidContent(anyhow!(
358            "Error: could not parse stake pool snapshot {stake_pool_snapshot:?}"
359        )))
360    }
361
362    // This is the legacy way of computing stake distribution, not optimized for mainnet, and usable for versions of the Cardano node up to '1.35.7'
363    async fn get_current_stake_distribution_legacy(
364        &self,
365    ) -> Result<Option<StakeDistribution>, ChainObserverError> {
366        let output = self
367            .cli_runner
368            .launch_stake_distribution()
369            .await
370            .map_err(ChainObserverError::General)?;
371        let mut stake_distribution = StakeDistribution::new();
372
373        for (num, line) in output.lines().enumerate() {
374            let words: Vec<&str> = line.split_ascii_whitespace().collect();
375
376            if num < 2 || words.len() != 2 {
377                continue;
378            }
379
380            let stake_pool_id = words[0];
381            let stake_fraction = words[1];
382
383            if let Ok((_, _f)) = self.parse_string(stake_fraction) {
384                // This block is a fix:
385                // the stake retrieved was computed on the current epoch, when we need a value computed on the previous epoch
386                // in 'let stake: u64 = (f * 1_000_000_000.0).round() as u64;'
387                let stake: u64 = self.get_current_stake_value(stake_pool_id).await?;
388
389                if stake > 0 {
390                    let _ = stake_distribution.insert(stake_pool_id.to_string(), stake);
391                }
392            } else {
393                return Err(ChainObserverError::InvalidContent(anyhow!(
394                    "could not parse stake from '{}'",
395                    words[1]
396                )));
397            }
398        }
399
400        Ok(Some(stake_distribution))
401    }
402
403    // This is the new way of computing stake distribution, optimized for mainnet, and usable for versions of the Cardano node from '8.0.0'
404    async fn get_current_stake_distribution_optimized(
405        &self,
406    ) -> Result<Option<StakeDistribution>, ChainObserverError> {
407        let output = self
408            .cli_runner
409            .launch_stake_snapshot_all_pools()
410            .await
411            .map_err(ChainObserverError::General)?;
412        let mut stake_distribution = StakeDistribution::new();
413
414        let data: HashMap<String, Value> =
415            serde_json::from_str(&output).map_err(|e| ChainObserverError::General(e.into()))?;
416        let pools_data = data
417            .get("pools")
418            .ok_or(ChainObserverError::InvalidContent(anyhow!(
419                "Missing 'pools' field"
420            )))?
421            .as_object()
422            .ok_or(ChainObserverError::InvalidContent(anyhow!(
423                "Could not convert pool data to object"
424            )))?;
425
426        for (k, v) in pools_data.iter() {
427            let pool_id_hex = k;
428            let pool_id_bech32 = encode_bech32(
429                "pool",
430                &Vec::from_hex(pool_id_hex.as_bytes())
431                    .map_err(|e| ChainObserverError::General(e.into()))?,
432            )
433            .map_err(ChainObserverError::General)?;
434            let stakes = v
435                .get("stakeMark")
436                .ok_or(ChainObserverError::InvalidContent(anyhow!(
437                    "Missing 'stakeMark' field for {pool_id_bech32}"
438                )))?
439                .as_u64()
440                .ok_or(ChainObserverError::InvalidContent(anyhow!(
441                    "Stake could not be converted to integer for {pool_id_bech32}"
442                )))?;
443            if stakes > 0 {
444                stake_distribution.insert(pool_id_bech32, stakes);
445            }
446        }
447
448        Ok(Some(stake_distribution))
449    }
450}
451
452#[async_trait]
453impl ChainObserver for CardanoCliChainObserver {
454    async fn get_current_era(&self) -> Result<Option<String>, ChainObserverError> {
455        let output = self
456            .cli_runner
457            .launch_era()
458            .await
459            .map_err(ChainObserverError::General)?;
460        let v: Value = serde_json::from_str(&output)
461            .with_context(|| format!("output was = '{output}'"))
462            .map_err(ChainObserverError::InvalidContent)?;
463
464        if let Value::String(era) = &v["era"] {
465            Ok(Some(era.to_string()))
466        } else {
467            Ok(None)
468        }
469    }
470
471    async fn get_current_epoch(&self) -> Result<Option<Epoch>, ChainObserverError> {
472        let output = self
473            .cli_runner
474            .launch_epoch()
475            .await
476            .map_err(ChainObserverError::General)?;
477        let v: Value = serde_json::from_str(&output)
478            .with_context(|| format!("output was = '{output}'"))
479            .map_err(ChainObserverError::InvalidContent)?;
480
481        if let Value::Number(epoch) = &v["epoch"] {
482            Ok(epoch.as_u64().map(Epoch))
483        } else {
484            Ok(None)
485        }
486    }
487
488    async fn get_current_chain_point(&self) -> Result<Option<ChainPoint>, ChainObserverError> {
489        let output = self
490            .cli_runner
491            .launch_chain_point()
492            .await
493            .map_err(ChainObserverError::General)?;
494        let v: Value = serde_json::from_str(&output)
495            .with_context(|| format!("output was = '{output}'"))
496            .map_err(ChainObserverError::InvalidContent)?;
497
498        if let Value::String(hash) = &v["hash"] {
499            Ok(Some(ChainPoint {
500                slot_number: SlotNumber(v["slot"].as_u64().unwrap_or_default()),
501                block_number: BlockNumber(v["block"].as_u64().unwrap_or_default()),
502                block_hash: hash.to_string(),
503            }))
504        } else {
505            Ok(None)
506        }
507    }
508
509    async fn get_current_datums(
510        &self,
511        address: &ChainAddress,
512    ) -> Result<Vec<TxDatum>, ChainObserverError> {
513        let output = self
514            .cli_runner
515            .launch_utxo(address)
516            .await
517            .map_err(ChainObserverError::General)?;
518        let v: HashMap<String, Value> = serde_json::from_str(&output)
519            .with_context(|| format!("output was = '{output}'"))
520            .map_err(ChainObserverError::InvalidContent)?;
521
522        Ok(v.values()
523            .filter_map(|v| {
524                v.get("inlineDatum")
525                    .filter(|datum| !datum.is_null())
526                    .map(|datum| TxDatum(datum.to_string()))
527            })
528            .collect())
529    }
530
531    // TODO: This function implements a fallback mechanism to compute the stake distribution: new/optimized computation when available, legacy computation otherwise
532    async fn get_current_stake_distribution(
533        &self,
534    ) -> Result<Option<StakeDistribution>, ChainObserverError> {
535        match self.get_current_stake_distribution_optimized().await {
536            Ok(stake_distribution_maybe) => Ok(stake_distribution_maybe),
537            Err(_) => self.get_current_stake_distribution_legacy().await,
538        }
539    }
540
541    async fn get_current_kes_period(
542        &self,
543        opcert: &OpCert,
544    ) -> Result<Option<KESPeriod>, ChainObserverError> {
545        let dir = std::env::temp_dir().join("mithril_kes_period");
546        fs::create_dir_all(&dir).map_err(|e| ChainObserverError::General(e.into()))?;
547        let opcert_file = dir.join(format!("opcert_kes_period-{}", opcert.compute_hash()));
548        opcert
549            .to_file(&opcert_file)
550            .map_err(|e| ChainObserverError::General(e.into()))?;
551        let output = self
552            .cli_runner
553            .launch_kes_period(opcert_file.to_str().unwrap())
554            .await
555            .map_err(ChainObserverError::General)?;
556        let first_left_curly_bracket_index = output.find('{').unwrap_or_default();
557        let output_cleaned = output.split_at(first_left_curly_bracket_index).1;
558        let v: Value = serde_json::from_str(output_cleaned)
559            .with_context(|| format!("output was = '{output}'"))
560            .map_err(ChainObserverError::InvalidContent)?;
561
562        if let Value::Number(kes_period) = &v["qKesCurrentKesPeriod"] {
563            Ok(kes_period.as_u64().map(|p| p as KESPeriod))
564        } else {
565            Ok(None)
566        }
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use std::collections::BTreeMap;
573
574    use super::*;
575    use crate::{
576        chain_observer::test_cli_runner::{test_expected, TestCliRunner},
577        crypto_helper::ColdKeyGenerator,
578    };
579
580    use kes_summed_ed25519::{kes::Sum6Kes, traits::KesSk};
581
582    #[tokio::test]
583    async fn test_get_current_era() {
584        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
585        let era = observer.get_current_era().await.unwrap().unwrap();
586
587        assert_eq!(test_expected::launch_era::ERA.to_string(), era);
588    }
589
590    #[tokio::test]
591    async fn test_get_current_epoch() {
592        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
593        let epoch = observer.get_current_epoch().await.unwrap().unwrap();
594
595        assert_eq!(test_expected::launch_epoch::EPOCH, epoch);
596    }
597
598    #[tokio::test]
599    async fn test_get_current_chain_point() {
600        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
601        let chain_point = observer.get_current_chain_point().await.unwrap().unwrap();
602
603        assert_eq!(
604            ChainPoint {
605                slot_number: test_expected::launch_chain_point::SLOT_NUMBER,
606                block_number: test_expected::launch_chain_point::BLOCK_NUMBER,
607                block_hash: test_expected::launch_chain_point::BLOCK_HASH.to_string(),
608            },
609            chain_point
610        );
611    }
612
613    #[tokio::test]
614    async fn test_cli_testnet_runner() {
615        let runner = CardanoCliRunner::new(
616            PathBuf::from("cardano-cli"),
617            PathBuf::from("/tmp/whatever.sock"),
618            CardanoNetwork::TestNet(10),
619        );
620
621        assert_eq!("Command { std: CARDANO_NODE_SOCKET_PATH=\"/tmp/whatever.sock\" \"cardano-cli\" \"latest\" \"query\" \"tip\" \"--testnet-magic\" \"10\", kill_on_drop: false }", format!("{:?}", runner.command_for_epoch()));
622        assert_eq!("Command { std: CARDANO_NODE_SOCKET_PATH=\"/tmp/whatever.sock\" \"cardano-cli\" \"latest\" \"query\" \"stake-distribution\" \"--testnet-magic\" \"10\", kill_on_drop: false }", format!("{:?}", runner.command_for_stake_distribution()));
623    }
624
625    #[tokio::test]
626    async fn test_cli_devnet_runner() {
627        let runner = CardanoCliRunner::new(
628            PathBuf::from("cardano-cli"),
629            PathBuf::from("/tmp/whatever.sock"),
630            CardanoNetwork::DevNet(25),
631        );
632
633        assert_eq!("Command { std: CARDANO_NODE_SOCKET_PATH=\"/tmp/whatever.sock\" \"cardano-cli\" \"latest\" \"query\" \"tip\" \"--cardano-mode\" \"--testnet-magic\" \"25\", kill_on_drop: false }", format!("{:?}", runner.command_for_epoch()));
634        assert_eq!("Command { std: CARDANO_NODE_SOCKET_PATH=\"/tmp/whatever.sock\" \"cardano-cli\" \"latest\" \"query\" \"stake-distribution\" \"--cardano-mode\" \"--testnet-magic\" \"25\", kill_on_drop: false }", format!("{:?}", runner.command_for_stake_distribution()));
635    }
636
637    #[tokio::test]
638    async fn test_cli_mainnet_runner() {
639        let runner = CardanoCliRunner::new(
640            PathBuf::from("cardano-cli"),
641            PathBuf::from("/tmp/whatever.sock"),
642            CardanoNetwork::MainNet,
643        );
644
645        assert_eq!(
646            "Command { std: CARDANO_NODE_SOCKET_PATH=\"/tmp/whatever.sock\" \"cardano-cli\" \"latest\" \"query\" \"tip\" \"--mainnet\", kill_on_drop: false }",
647            format!("{:?}", runner.command_for_epoch())
648        );
649        assert_eq!(
650            "Command { std: CARDANO_NODE_SOCKET_PATH=\"/tmp/whatever.sock\" \"cardano-cli\" \"latest\" \"query\" \"stake-distribution\" \"--mainnet\", kill_on_drop: false }",
651            format!("{:?}", runner.command_for_stake_distribution())
652        );
653    }
654
655    #[tokio::test]
656    async fn test_get_current_datums() {
657        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
658        let address = "addrtest_123456".to_string();
659        let datums = observer.get_current_datums(&address).await.unwrap();
660        assert_eq!(
661            vec![TxDatum(
662                format!(
663                    r#"{{"constructor":0,"fields":[{{"bytes":"{}"}}]}}"#,
664                    test_expected::launch_utxo::BYTES
665                )
666                .to_string()
667            )],
668            datums
669        );
670    }
671
672    #[tokio::test]
673    async fn test_get_current_stake_value() {
674        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
675        let stake = observer
676            .get_current_stake_value("pool1qqyjr9pcrv97gwrueunug829fs5znw6p2wxft3fvqkgu5f4qlrg")
677            .await
678            .expect("get current stake value should not fail");
679        assert_eq!(
680            test_expected::launch_stake_snapshot::DEFAULT_POOL_STAKE_MARK,
681            stake
682        );
683
684        let stake = observer
685            .get_current_stake_value(test_expected::launch_stake_snapshot::POOL_ID_SPECIFIC)
686            .await
687            .expect("get current stake value should not fail");
688        assert_eq!(
689            test_expected::launch_stake_snapshot::POOL_STAKE_MARK_FOR_POOL_ID_SPECIFIC,
690            stake
691        );
692    }
693
694    #[tokio::test]
695    async fn test_get_current_stake_distribution_legacy() {
696        let observer = CardanoCliChainObserver::new(Box::new(TestCliRunner::legacy()));
697        let results = observer
698            .get_current_stake_distribution_legacy()
699            .await
700            .unwrap()
701            .unwrap();
702        assert_eq!(7, results.len());
703        assert_eq!(
704            3_000_000,
705            *results
706                .get("pool1qqyjr9pcrv97gwrueunug829fs5znw6p2wxft3fvqkgu5f4qlrg")
707                .unwrap()
708        );
709        assert_eq!(
710            3_000_000,
711            *results
712                .get("pool1qz2vzszautc2c8mljnqre2857dpmheq7kgt6vav0s38tvvhxm6w")
713                .unwrap()
714        );
715        assert!(!results.contains_key("pool1qpqvz90w7qsex2al2ejjej0rfgrwsguch307w8fraw7a7adf6g8"));
716    }
717
718    #[tokio::test]
719    async fn test_get_current_stake_distribution_new() {
720        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
721        let computed_stake_distribution = observer
722            .get_current_stake_distribution_optimized()
723            .await
724            .unwrap()
725            .unwrap();
726        let mut expected_stake_distribution = StakeDistribution::new();
727        expected_stake_distribution.insert(
728            "pool1qqqqqdk4zhsjuxxd8jyvwncf5eucfskz0xjjj64fdmlgj735lr9".to_string(),
729            test_expected::launch_stake_snapshot_all_pools::STAKE_MARK_POOL_1,
730        );
731        expected_stake_distribution.insert(
732            "pool1qqqqpanw9zc0rzh0yp247nzf2s35uvnsm7aaesfl2nnejaev0uc".to_string(),
733            test_expected::launch_stake_snapshot_all_pools::STAKE_MARK_POOL_2,
734        );
735        expected_stake_distribution.insert(
736            "pool1qqqqzyqf8mlm70883zht60n4q6uqxg4a8x266sewv8ad2grkztl".to_string(),
737            test_expected::launch_stake_snapshot_all_pools::STAKE_MARK_POOL_3,
738        );
739        assert_eq!(
740            BTreeMap::from_iter(
741                expected_stake_distribution
742                    .into_iter()
743                    .collect::<Vec<(_, _)>>()
744                    .into_iter(),
745            ),
746            BTreeMap::from_iter(
747                computed_stake_distribution
748                    .into_iter()
749                    .collect::<Vec<(_, _)>>()
750                    .into_iter(),
751            ),
752        );
753    }
754
755    #[tokio::test]
756    async fn test_get_current_stake_distribution() {
757        let observer = CardanoCliChainObserver::new(Box::new(TestCliRunner::legacy()));
758        let expected_stake_distribution = observer
759            .get_current_stake_distribution_legacy()
760            .await
761            .unwrap()
762            .unwrap();
763        let computed_stake_distribution = observer
764            .get_current_stake_distribution()
765            .await
766            .unwrap()
767            .unwrap();
768
769        assert_eq!(
770            BTreeMap::from_iter(
771                expected_stake_distribution
772                    .clone()
773                    .into_iter()
774                    .collect::<Vec<(_, _)>>()
775                    .into_iter(),
776            ),
777            BTreeMap::from_iter(
778                computed_stake_distribution
779                    .into_iter()
780                    .collect::<Vec<(_, _)>>()
781                    .into_iter(),
782            ),
783        );
784
785        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
786        let expected_stake_distribution = observer
787            .get_current_stake_distribution_optimized()
788            .await
789            .unwrap()
790            .unwrap();
791        let computed_stake_distribution = observer
792            .get_current_stake_distribution()
793            .await
794            .unwrap()
795            .unwrap();
796
797        assert_eq!(
798            BTreeMap::from_iter(
799                expected_stake_distribution
800                    .into_iter()
801                    .collect::<Vec<(_, _)>>()
802                    .into_iter(),
803            ),
804            BTreeMap::from_iter(
805                computed_stake_distribution
806                    .into_iter()
807                    .collect::<Vec<(_, _)>>()
808                    .into_iter(),
809            ),
810        );
811    }
812
813    #[tokio::test]
814    async fn test_get_current_kes_period() {
815        let keypair = ColdKeyGenerator::create_deterministic_keypair([0u8; 32]);
816        let mut dummy_key_buffer = [0u8; Sum6Kes::SIZE + 4];
817        let mut dummy_seed = [0u8; 32];
818        let (_, kes_verification_key) = Sum6Kes::keygen(&mut dummy_key_buffer, &mut dummy_seed);
819        let operational_certificate = OpCert::new(kes_verification_key, 0, 0, keypair);
820        let observer = CardanoCliChainObserver::new(Box::<TestCliRunner>::default());
821        let kes_period = observer
822            .get_current_kes_period(&operational_certificate)
823            .await
824            .unwrap()
825            .unwrap();
826        assert_eq!(test_expected::launch_kes_period::KES_PERIOD, kes_period);
827    }
828}