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#[async_trait]
23pub trait CliRunner {
24 async fn launch_utxo(&self, address: &str) -> StdResult<String>;
26 async fn launch_stake_distribution(&self) -> StdResult<String>;
28 async fn launch_stake_snapshot(&self, stake_pool_id: &str) -> StdResult<String>;
30 async fn launch_stake_snapshot_all_pools(&self) -> StdResult<String>;
32 async fn launch_era(&self) -> StdResult<String>;
34 async fn launch_epoch(&self) -> StdResult<String>;
36 async fn launch_chain_point(&self) -> StdResult<String>;
38 async fn launch_kes_period(&self, opcert_file: &str) -> StdResult<String>;
40}
41
42#[derive(Clone, Debug)]
45pub struct CardanoCliRunner {
46 cli_path: PathBuf,
47 socket_path: PathBuf,
48 network: CardanoNetwork,
49}
50
51impl CardanoCliRunner {
52 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
321pub struct CardanoCliChainObserver {
323 cli_runner: Box<dyn CliRunner + Send + Sync>,
324}
325
326impl CardanoCliChainObserver {
327 pub fn new(cli_runner: Box<dyn CliRunner + Send + Sync>) -> Self {
329 Self { cli_runner }
330 }
331
332 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 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 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 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 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}