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