1use std::{collections::HashMap, num::NonZero, str::FromStr};
7
8use anyhow::Context as _;
9use futures::stream::TryStreamExt as _;
10use linera_base::{
11 crypto::{AccountPublicKey, ValidatorPublicKey},
12 data_types::BlockHeight,
13 identifiers::ChainId,
14};
15use linera_client::{chain_listener::ClientContext as _, client_context::ClientContext};
16use linera_core::{
17 data_types::ClientOutcome,
18 node::{ValidatorNode, ValidatorNodeProvider},
19 Wallet as _,
20};
21use linera_execution::committee::{Committee, ValidatorState};
22use serde::{Deserialize, Serialize};
23
24use crate::cli::validator_benchmark::Benchmark;
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
30pub struct Votes(pub NonZero<u64>);
31
32impl Default for Votes {
33 fn default() -> Self {
34 Self(nonzero_lit::u64!(1))
35 }
36}
37
38impl FromStr for Votes {
39 type Err = <NonZero<u64> as FromStr>::Err;
40 fn from_str(s: &str) -> Result<Self, Self::Err> {
41 Ok(Votes(s.parse()?))
42 }
43}
44
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct Spec {
49 pub public_key: ValidatorPublicKey,
51 pub account_key: AccountPublicKey,
53 pub network_address: url::Url,
55 #[serde(default)]
57 pub votes: Votes,
58}
59
60#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Change {
64 pub account_key: AccountPublicKey,
66 pub address: url::Url,
68 #[serde(default)]
70 pub votes: Votes,
71}
72
73pub type BatchFile = HashMap<ValidatorPublicKey, Option<Change>>;
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct QueryBatch {
83 pub validators: Vec<Spec>,
85}
86
87#[derive(Debug, Clone, clap::Subcommand)]
92#[allow(missing_docs)]
93pub enum Command {
94 Add(Add),
95 BatchQuery(BatchQuery),
96 Benchmark(Benchmark),
97 Update(Update),
98 List(List),
99 Query(Query),
100 QueryBlock(QueryBlock),
101 Remove(Remove),
102 Sync(Sync),
103}
104
105#[derive(Debug, Clone, clap::Parser)]
110pub struct Add {
111 #[arg(long)]
113 public_key: ValidatorPublicKey,
114 #[arg(long)]
116 account_key: AccountPublicKey,
117 #[arg(long)]
119 address: url::Url,
120 #[arg(long, required = false)]
122 votes: Votes,
123 #[arg(long)]
125 skip_online_check: bool,
126}
127
128#[derive(Debug, Clone, clap::Parser)]
133pub struct BatchQuery {
134 file: clio::Input,
136 #[arg(long)]
138 chain_id: Option<ChainId>,
139}
140
141#[derive(Debug, Clone, clap::Parser)]
150pub struct Update {
151 #[arg(required = false)]
153 file: clio::Input,
154 #[arg(long)]
156 dry_run: bool,
157 #[arg(long, short = 'y')]
159 yes: bool,
160 #[arg(long)]
162 skip_online_check: bool,
163}
164
165#[derive(Debug, Clone, clap::Parser)]
170pub struct List {
171 #[arg(long)]
173 chain_id: Option<ChainId>,
174 #[arg(long)]
176 min_votes: Option<u64>,
177}
178
179#[derive(Debug, Clone, clap::Parser)]
184pub struct Query {
185 address: String,
187 #[arg(long)]
189 chain_id: Option<ChainId>,
190 #[arg(long)]
192 public_key: Option<ValidatorPublicKey>,
193}
194
195#[derive(Debug, Clone, clap::Parser)]
200pub struct QueryBlock {
201 address: String,
203 #[arg(long)]
205 chain_id: Option<ChainId>,
206 #[arg(long)]
208 public_key: Option<ValidatorPublicKey>,
209 #[arg(long)]
211 height: BlockHeight,
212}
213
214#[derive(Debug, Clone, clap::Parser)]
219pub struct Remove {
220 #[arg(long)]
222 public_key: ValidatorPublicKey,
223}
224
225#[derive(Debug, Clone, clap::Parser)]
230pub struct Sync {
231 address: String,
233 #[arg(long)]
235 chains: Vec<ChainId>,
236 #[arg(long)]
238 check_online: bool,
239 #[arg(long)]
243 public_key: Option<ValidatorPublicKey>,
244}
245
246fn parse_batch_file(input: clio::Input) -> anyhow::Result<BatchFile> {
249 Ok(serde_json::from_reader(input)?)
250}
251
252fn parse_query_batch_file(input: clio::Input) -> anyhow::Result<QueryBatch> {
254 Ok(serde_json::from_reader(input)?)
255}
256
257impl Command {
258 pub async fn run(
260 &self,
261 context: &mut ClientContext<
262 impl linera_core::Environment<ValidatorNode = linera_rpc::Client>,
263 >,
264 ) -> anyhow::Result<()> {
265 use Command::*;
266
267 match self {
268 Add(command) => command.run(context).await,
269 BatchQuery(command) => Box::pin(command.run(context)).await,
270 Benchmark(command) => Box::pin(command.run(context)).await,
271 Update(command) => command.run(context).await,
272 List(command) => command.run(context).await,
273 Query(command) => command.run(context).await,
274 QueryBlock(command) => command.run(context).await,
275 Remove(command) => command.run(context).await,
276 Sync(command) => Box::pin(command.run(context)).await,
277 }
278 }
279}
280
281impl Add {
282 async fn run(
283 &self,
284 context: &mut ClientContext<impl linera_core::Environment>,
285 ) -> anyhow::Result<()> {
286 tracing::info!("Starting operation to add validator");
287 let time_start = std::time::Instant::now();
288
289 if !self.skip_online_check {
291 let node = context
292 .make_node_provider()
293 .make_node(self.address.as_str())?;
294 context
295 .check_compatible_version_info(self.address.as_str(), &node)
296 .await?;
297 context
298 .check_matching_network_description(self.address.as_str(), &node)
299 .await?;
300 }
301
302 let admin_chain_id = context.admin_chain_id();
303 let chain_client = context.make_chain_client(admin_chain_id).await?;
304
305 chain_client.synchronize_chain_state(admin_chain_id).await?;
307
308 let maybe_certificate = context
309 .apply_client_command(&chain_client, |chain_client| {
310 let me = self.clone();
311 let chain_client = chain_client.clone();
312 async move {
313 let committee = chain_client.local_committee().await?;
315 let policy = committee.policy().clone();
316 let mut validators = committee.validators().clone();
317
318 validators.insert(
319 me.public_key,
320 ValidatorState {
321 network_address: me.address.to_string(),
322 votes: me.votes.0.get(),
323 account_public_key: me.account_key,
324 },
325 );
326
327 let new_committee = Committee::new(validators, policy)?;
328 chain_client
329 .stage_new_committee(new_committee)
330 .await
331 .map(|outcome| outcome.map(Some))
332 }
333 })
334 .await
335 .context("Failed to stage committee")?;
336
337 let Some(certificate) = maybe_certificate else {
338 return Ok(());
339 };
340 tracing::info!("Created new committee:\n{:?}", certificate);
341
342 let time_total = time_start.elapsed();
343 tracing::info!("Operation confirmed after {} ms", time_total.as_millis());
344
345 Ok(())
346 }
347}
348
349impl BatchQuery {
350 async fn run(
351 &self,
352 context: &ClientContext<impl linera_core::Environment>,
353 ) -> anyhow::Result<()> {
354 let batch = parse_query_batch_file(self.file.clone())
355 .context("parsing query batch file `{file}`")?;
356 let chain_id = self.chain_id.unwrap_or_else(|| context.default_chain());
357 println!(
358 "Querying {} validators about chain {chain_id}.\n",
359 batch.validators.len()
360 );
361
362 let node_provider = context.make_node_provider();
363 let mut has_errors = false;
364
365 for spec in batch.validators {
366 let node = node_provider.make_node(spec.network_address.as_str())?;
367 let results = context
368 .query_validator(
369 spec.network_address.as_str(),
370 &node,
371 chain_id,
372 Some(&spec.public_key),
373 )
374 .await;
375
376 if !results.errors().is_empty() {
377 has_errors = true;
378 for error in results.errors() {
379 tracing::error!("Validator {}: {}", spec.public_key, error);
380 }
381 }
382
383 results.print(
384 Some(&spec.public_key),
385 Some(spec.network_address.as_str()),
386 None,
387 None,
388 );
389 }
390
391 if has_errors {
392 anyhow::bail!("Found issues while querying validators");
393 }
394
395 Ok(())
396 }
397}
398
399impl Update {
400 async fn run(
401 &self,
402 context: &mut ClientContext<impl linera_core::Environment>,
403 ) -> anyhow::Result<()> {
404 tracing::info!("Starting batch update operation");
405 let time_start = std::time::Instant::now();
406
407 let batch = parse_batch_file(self.file.clone())
409 .with_context(|| format!("parsing batch file `{}`", self.file))?;
410
411 if batch.is_empty() {
412 tracing::warn!("No validator changes specified in input.");
413 return Ok(());
414 }
415
416 let mut adds = Vec::new();
418 let mut modifies = Vec::new();
419 let mut removes = Vec::new();
420
421 let admin_chain_id = context.client().admin_chain_id();
423 let chain_client = context.make_chain_client(admin_chain_id).await?;
424 let current_committee = chain_client.local_committee().await?;
425 let current_validators = current_committee.validators();
426
427 for (public_key, change_opt) in &batch {
428 match change_opt {
429 None => {
430 removes.push(*public_key);
432 }
433 Some(spec) => {
434 if current_validators.contains_key(public_key) {
435 modifies.push((public_key, spec));
436 } else {
437 adds.push((public_key, spec));
438 }
439 }
440 }
441 }
442
443 println!(
445 "\n╔══════════════════════════════════════════════════════════════════════════════╗"
446 );
447 println!(
448 "║ VALIDATOR BATCH UPDATE RECAP ║"
449 );
450 println!(
451 "╚══════════════════════════════════════════════════════════════════════════════╝\n"
452 );
453
454 println!("Summary:");
455 println!(" • {} validator(s) to add", adds.len());
456 println!(" • {} validator(s) to modify", modifies.len());
457 println!(" • {} validator(s) to remove", removes.len());
458 println!();
459
460 if !adds.is_empty() {
461 println!("Validators to ADD:");
462 for (pk, spec) in &adds {
463 println!(" + {pk}");
464 println!(" Address: {}", spec.address);
465 println!(" Account Key: {}", spec.account_key);
466 println!(" Votes: {}", spec.votes.0.get());
467 }
468 println!();
469 }
470
471 if !modifies.is_empty() {
472 println!("Validators to MODIFY:");
473 for (pk, spec) in &modifies {
474 println!(" * {pk}");
475 println!(" New Address: {}", spec.address);
476 println!(" New Account Key: {}", spec.account_key);
477 println!(" New Votes: {}", spec.votes.0.get());
478 }
479 println!();
480 }
481
482 if !removes.is_empty() {
483 println!("Validators to REMOVE:");
484 for pk in &removes {
485 println!(" - {pk}");
486 }
487 println!();
488 }
489
490 if self.dry_run {
491 println!(
492 "═════════════════════════════════════════════════════════════════════════════"
493 );
494 println!("DRY RUN MODE: No changes will be applied");
495 println!(
496 "═════════════════════════════════════════════════════════════════════════════\n"
497 );
498 return Ok(());
499 }
500
501 if !self.yes {
503 println!(
504 "═════════════════════════════════════════════════════════════════════════════"
505 );
506 println!("⚠️ WARNING: This operation will modify the validator committee.");
507 println!(" Changes are permanent and will be broadcast to the network.");
508 println!(
509 "═════════════════════════════════════════════════════════════════════════════\n"
510 );
511 println!("Do you want to proceed? Type 'YES' (uppercase) to confirm: ");
512
513 use std::io::{self, Write};
514 io::stdout().flush()?;
515
516 let mut input = String::new();
517 io::stdin()
518 .read_line(&mut input)
519 .context("Failed to read confirmation input")?;
520
521 let input = input.trim();
522 if input != "YES" {
523 println!("\nOperation cancelled. (Expected 'YES', got '{input}')");
524 return Ok(());
525 }
526 println!("\nConfirmed. Proceeding with batch update...\n");
527 }
528
529 if !self.skip_online_check {
531 let node_provider = context.make_node_provider();
532
533 tracing::info!("Checking validators are online...");
534 for (_, spec) in adds.iter().chain(modifies.iter()) {
535 let address = &spec.address;
536 let node = node_provider.make_node(address.as_str())?;
537 context
538 .check_compatible_version_info(address.as_str(), &node)
539 .await?;
540 context
541 .check_matching_network_description(address.as_str(), &node)
542 .await?;
543 }
544 }
545
546 let admin_chain_id = context.admin_chain_id();
547 let chain_client = context.make_chain_client(admin_chain_id).await?;
548
549 chain_client.synchronize_chain_state(admin_chain_id).await?;
551
552 let batch_clone = batch.clone();
553 let maybe_certificate = context
554 .apply_client_command(&chain_client, |chain_client| {
555 let chain_client = chain_client.clone();
556 let batch = batch_clone.clone();
557 async move {
558 let committee = chain_client.local_committee().await?;
560 let policy = committee.policy().clone();
561 let mut validators = committee.validators().clone();
562
563 for (public_key, change_opt) in &batch {
565 if let Some(spec) = change_opt {
566 let address = &spec.address;
568 let votes = spec.votes.0.get();
569 let account_key = spec.account_key;
570
571 let exists = validators.contains_key(public_key);
572 validators.insert(
573 *public_key,
574 ValidatorState {
575 network_address: address.to_string(),
576 votes,
577 account_public_key: account_key,
578 },
579 );
580
581 if exists {
582 tracing::info!(
583 "Modified validator {} @ {} ({} votes)",
584 public_key,
585 address,
586 votes
587 );
588 } else {
589 tracing::info!(
590 "Added validator {} @ {} ({} votes)",
591 public_key,
592 address,
593 votes
594 );
595 }
596 } else {
597 if validators.remove(public_key).is_none() {
599 tracing::warn!(
600 "Validator {} does not exist; skipping remove",
601 public_key
602 );
603 } else {
604 tracing::info!("Removed validator {}", public_key);
605 }
606 }
607 }
608
609 let new_committee = Committee::new(validators, policy)?;
611 chain_client
612 .stage_new_committee(new_committee)
613 .await
614 .map(|outcome| outcome.map(Some))
615 }
616 })
617 .await
618 .context("Failed to stage committee")?;
619
620 let Some(certificate) = maybe_certificate else {
621 tracing::info!("No changes applied");
622 return Ok(());
623 };
624
625 tracing::info!("Created new committee:\n{:?}", certificate);
626 let time_total = time_start.elapsed();
627 tracing::info!("Batch update confirmed after {} ms", time_total.as_millis());
628
629 Ok(())
630 }
631}
632
633impl List {
634 async fn run(
635 &self,
636 context: &ClientContext<impl linera_core::Environment>,
637 ) -> anyhow::Result<()> {
638 let chain_id = self.chain_id.unwrap_or_else(|| context.default_chain());
639 println!("Querying validators about chain {chain_id}.\n");
640
641 let local_results = context.query_local_node(chain_id).await?;
642 let chain_client = context.make_chain_client(chain_id).await?;
643 tracing::info!("Querying validators about chain {}", chain_id);
644 let result = chain_client.local_committee().await;
645 context.update_wallet_from_client(&chain_client).await?;
646 let committee = result.context("Failed to get local committee")?;
647
648 tracing::info!(
649 "Using the local set of validators: {:?}",
650 committee.validators()
651 );
652
653 let node_provider = context.make_node_provider();
654 let mut validator_results = Vec::new();
655
656 for (name, state) in committee.validators() {
657 if self.min_votes.is_some_and(|votes| state.votes < votes) {
658 continue; }
660 let address = &state.network_address;
661 let node = node_provider.make_node(address)?;
662 let results = context
663 .query_validator(address, &node, chain_id, Some(name))
664 .await;
665 validator_results.push((name, address, state.votes, results));
666 }
667
668 let mut faulty_validators = std::collections::BTreeMap::<_, Vec<_>>::new();
669 for (name, address, _votes, results) in &validator_results {
670 for error in results.errors() {
671 tracing::error!("{}", error);
672 faulty_validators
673 .entry((*name, *address))
674 .or_default()
675 .push(error);
676 }
677 }
678
679 println!("Local Node:");
681 local_results.print(None, None, None, None);
682
683 for (name, address, votes, results) in &validator_results {
685 results.print(
686 Some(name),
687 Some(address),
688 Some(*votes),
689 Some(&local_results),
690 );
691 }
692
693 if !faulty_validators.is_empty() {
694 println!("\nFaulty validators:");
695 for ((name, address), errors) in faulty_validators {
696 println!(" {} at {}: {} error(s)", name, address, errors.len());
697 }
698 anyhow::bail!("Found faulty validators");
699 }
700
701 Ok(())
702 }
703}
704
705impl Query {
706 async fn run(
707 &self,
708 context: &ClientContext<impl linera_core::Environment>,
709 ) -> anyhow::Result<()> {
710 let node = context.make_node_provider().make_node(&self.address)?;
711 let chain_id = self.chain_id.unwrap_or_else(|| context.default_chain());
712 println!("Querying validator about chain {chain_id}.\n");
713
714 let results = context
715 .query_validator(&self.address, &node, chain_id, self.public_key.as_ref())
716 .await;
717
718 for error in results.errors() {
719 tracing::error!("{}", error);
720 }
721
722 results.print(self.public_key.as_ref(), Some(&self.address), None, None);
723
724 if !results.errors().is_empty() {
725 anyhow::bail!(
726 "Found one or several issue(s) while querying validator {}",
727 self.address
728 );
729 }
730
731 Ok(())
732 }
733}
734
735impl QueryBlock {
736 async fn run(
737 &self,
738 context: &ClientContext<impl linera_core::Environment>,
739 ) -> anyhow::Result<()> {
740 let node = context.make_node_provider().make_node(&self.address)?;
741 let chain_id = self.chain_id.unwrap_or_else(|| context.default_chain());
742 let height = self.height;
743 println!(
744 "Querying validator about the certificate for height {height} on the chain \
745 {chain_id}.\n"
746 );
747
748 let result = node
749 .download_certificates_by_heights(chain_id, vec![height])
750 .await;
751
752 match result {
753 Ok(certificates) => {
754 let confirmed_block = certificates[0].inner();
755 println!("{confirmed_block:#?}");
756 }
757 Err(error) => {
758 tracing::error!("{}", error);
759 }
760 }
761
762 Ok(())
763 }
764}
765
766impl Remove {
767 async fn run(
768 &self,
769 context: &mut ClientContext<impl linera_core::Environment>,
770 ) -> anyhow::Result<()> {
771 tracing::info!("Starting operation to remove validator");
772 let time_start = std::time::Instant::now();
773
774 let admin_chain_id = context.admin_chain_id();
775 let chain_client = context.make_chain_client(admin_chain_id).await?;
776
777 chain_client.synchronize_chain_state(admin_chain_id).await?;
779
780 let maybe_certificate = context
781 .apply_client_command(&chain_client, |chain_client| {
782 let chain_client = chain_client.clone();
783 async move {
784 let committee = chain_client.local_committee().await?;
786 let policy = committee.policy().clone();
787 let mut validators = committee.validators().clone();
788
789 if validators.remove(&self.public_key).is_none() {
790 tracing::error!("Validator {} does not exist; aborting.", self.public_key);
791 return Ok(ClientOutcome::Committed(None));
792 }
793
794 let new_committee = Committee::new(validators, policy)?;
795 chain_client
796 .stage_new_committee(new_committee)
797 .await
798 .map(|outcome| outcome.map(Some))
799 }
800 })
801 .await
802 .context("Failed to stage committee")?;
803
804 let Some(certificate) = maybe_certificate else {
805 return Ok(());
806 };
807 tracing::info!("Created new committee:\n{:?}", certificate);
808
809 let time_total = time_start.elapsed();
810 tracing::info!("Operation confirmed after {} ms", time_total.as_millis());
811
812 Ok(())
813 }
814}
815
816impl Sync {
817 async fn run(
818 &self,
819 context: &ClientContext<impl linera_core::Environment<ValidatorNode = linera_rpc::Client>>,
820 ) -> anyhow::Result<()> {
821 tracing::info!("Starting sync operation for validator at {}", self.address);
822
823 if self.check_online {
825 let node_provider = context.make_node_provider();
826 let node = node_provider.make_node(&self.address)?;
827 context
828 .check_compatible_version_info(&self.address, &node)
829 .await?;
830 context
831 .check_matching_network_description(&self.address, &node)
832 .await?;
833 }
834
835 let chains_to_sync = if self.chains.is_empty() {
837 context.wallet().chain_ids().try_collect().await?
838 } else {
839 self.chains.clone()
840 };
841
842 tracing::info!(
843 "Syncing {} chains to validator {}",
844 chains_to_sync.len(),
845 self.address
846 );
847
848 let node_provider = context.make_node_provider();
850 let validator = node_provider.make_node(&self.address)?;
851
852 let public_key = match self.public_key {
855 Some(public_key) => public_key,
856 None => {
857 let admin_chain = context.make_chain_client(context.admin_chain_id()).await?;
858 let (_, committee) = admin_chain.admin_committee().await?;
859 let public_key = committee
860 .validator_addresses()
861 .find(|(_, address)| *address == self.address)
862 .map(|(public_key, _)| public_key);
863 public_key.with_context(|| {
864 format!(
865 "validator {} is not in the current committee; \
866 use --public-key to sync it",
867 self.address
868 )
869 })?
870 }
871 };
872
873 for chain_id in chains_to_sync {
875 tracing::info!("Syncing chain {} to {}", chain_id, self.address);
876 let chain = context.make_chain_client(chain_id).await?;
877
878 Box::pin(chain.sync_validator(public_key, validator.clone())).await?;
879 tracing::info!("Chain {} synced successfully", chain_id);
880 }
881
882 tracing::info!("Sync operation completed successfully");
883 Ok(())
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use std::io::Write;
890
891 use tempfile::NamedTempFile;
892
893 use super::*;
894
895 #[test]
896 fn test_parse_batch_file_valid() {
897 let pk0 = ValidatorPublicKey::test_key(0);
899 let pk1 = ValidatorPublicKey::test_key(1);
900 let pk2 = ValidatorPublicKey::test_key(2);
901
902 let mut batch = BatchFile::new();
903
904 batch.insert(
906 pk0,
907 Some(Change {
908 account_key: AccountPublicKey::test_key(0),
909 address: "grpcs://validator1.example.com:443".parse().unwrap(),
910 votes: Votes(NonZero::new(100).unwrap()),
911 }),
912 );
913
914 batch.insert(
916 pk1,
917 Some(Change {
918 account_key: AccountPublicKey::test_key(1),
919 address: "grpcs://validator2.example.com:443".parse().unwrap(),
920 votes: Votes(NonZero::new(150).unwrap()),
921 }),
922 );
923
924 batch.insert(pk2, None);
926
927 let json = serde_json::to_string(&batch).unwrap();
928
929 let mut temp_file = NamedTempFile::new().unwrap();
930 temp_file.write_all(json.as_bytes()).unwrap();
931 temp_file.flush().unwrap();
932
933 let input = clio::Input::new(temp_file.path().to_str().unwrap()).unwrap();
934 let result = parse_batch_file(input);
935 assert!(
936 result.is_ok(),
937 "Failed to parse batch file: {:?}",
938 result.err()
939 );
940
941 let parsed_batch = result.unwrap();
942 assert_eq!(parsed_batch.len(), 3);
943
944 assert!(parsed_batch.contains_key(&pk0));
946 let spec0 = parsed_batch.get(&pk0).unwrap().as_ref().unwrap();
947 assert_eq!(spec0.votes.0.get(), 100);
948
949 assert!(parsed_batch.contains_key(&pk1));
951 let spec1 = parsed_batch.get(&pk1).unwrap().as_ref().unwrap();
952 assert_eq!(spec1.votes.0.get(), 150);
953
954 assert!(parsed_batch.contains_key(&pk2));
956 assert!(parsed_batch.get(&pk2).unwrap().is_none());
957 }
958
959 #[test]
960 fn test_parse_batch_file_empty() {
961 let json = r#"{}"#;
962
963 let mut temp_file = NamedTempFile::new().unwrap();
964 temp_file.write_all(json.as_bytes()).unwrap();
965 temp_file.flush().unwrap();
966
967 let input = clio::Input::new(temp_file.path().to_str().unwrap()).unwrap();
968 let result = parse_batch_file(input);
969 assert!(result.is_ok());
970
971 let batch = result.unwrap();
972 assert_eq!(batch.len(), 0);
973 }
974
975 #[test]
976 fn test_parse_query_batch_file_valid() {
977 let spec1 = Spec {
979 public_key: ValidatorPublicKey::test_key(0),
980 account_key: AccountPublicKey::test_key(0),
981 network_address: "grpcs://validator1.example.com:443".parse().unwrap(),
982 votes: Votes(NonZero::new(100).unwrap()),
983 };
984 let spec2 = Spec {
985 public_key: ValidatorPublicKey::test_key(1),
986 account_key: AccountPublicKey::test_key(1),
987 network_address: "grpcs://validator2.example.com:443".parse().unwrap(),
988 votes: Votes(NonZero::new(150).unwrap()),
989 };
990
991 let batch = QueryBatch {
992 validators: vec![spec1, spec2],
993 };
994
995 let json = serde_json::to_string(&batch).unwrap();
996
997 let mut temp_file = NamedTempFile::new().unwrap();
998 temp_file.write_all(json.as_bytes()).unwrap();
999 temp_file.flush().unwrap();
1000
1001 let result = parse_query_batch_file(temp_file.path().try_into().unwrap());
1002 assert!(
1003 result.is_ok(),
1004 "Failed to parse query batch file: {:?}",
1005 result.err()
1006 );
1007
1008 let parsed_batch = result.unwrap();
1009 assert_eq!(parsed_batch.validators.len(), 2);
1010 assert_eq!(parsed_batch.validators[0].votes.0.get(), 100);
1011 assert_eq!(parsed_batch.validators[1].votes.0.get(), 150);
1012 }
1013}