1use crate::{
2 cache_file::{
3 CacheFileError, JsonCacheReport, LoadJsonCacheErrorHandlers, LoadJsonCacheRequest,
4 RefreshCacheWriteRequest, load_json_cache, write_json_refresh_cache,
5 },
6 nns_render::{compact_text, text_or_dash, yes_no},
7 subnet_catalog::format_utc_timestamp_secs,
8 table::{ColumnAlign, render_table},
9};
10use canic_ic_registry::{
11 DEFAULT_MAINNET_ENDPOINT, MainnetNodeList, MainnetRegistryFetchRequest, RegistryFetchError,
12 fetch_mainnet_node_list,
13};
14use canic_subnet_catalog::{MAINNET_NETWORK, canonical_principal_text};
15use serde::{Deserialize, Serialize};
16use std::{
17 io,
18 path::{Path, PathBuf},
19};
20use thiserror::Error as ThisError;
21
22pub const DEFAULT_NNS_NODE_SOURCE_ENDPOINT: &str = DEFAULT_MAINNET_ENDPOINT;
23pub const DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS: u64 = 30 * 60;
24pub const NNS_NODE_LIST_REPORT_SCHEMA_VERSION: u32 = 1;
25pub const NNS_NODE_INFO_REPORT_SCHEMA_VERSION: u32 = 1;
26pub const NNS_NODE_REFRESH_REPORT_SCHEMA_VERSION: u32 = 1;
27const COMPACT_PRINCIPAL_CHARS: usize = 5;
28
29#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct NnsNodeCacheRequest {
34 pub icp_root: PathBuf,
35 pub network: String,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct NnsNodeListRequest {
43 pub cache: NnsNodeCacheRequest,
44 pub source_endpoint: String,
45 pub now_unix_secs: u64,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct NnsNodeInfoRequest {
53 pub cache: NnsNodeCacheRequest,
54 pub source_endpoint: String,
55 pub input: String,
56 pub now_unix_secs: u64,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct NnsNodeRefreshRequest {
64 pub cache: NnsNodeCacheRequest,
65 pub source_endpoint: String,
66 pub now_unix_secs: u64,
67 pub lock_stale_after_seconds: u64,
68 pub dry_run: bool,
69 pub output_path: Option<PathBuf>,
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct CachedNnsNodeReport {
77 pub path: PathBuf,
78 pub report: NnsNodeListReport,
79}
80
81#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
85pub struct NnsNodeListReport {
86 pub schema_version: u32,
87 pub network: String,
88 pub registry_canister_id: String,
89 pub registry_version: u64,
90 pub fetched_at: String,
91 pub source_endpoint: String,
92 pub fetched_by: String,
93 pub node_count: usize,
94 pub nodes: Vec<NnsNodeRow>,
95}
96
97impl JsonCacheReport for NnsNodeListReport {
98 fn schema_version(&self) -> u32 {
99 self.schema_version
100 }
101
102 fn network(&self) -> &str {
103 &self.network
104 }
105}
106
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
111pub struct NnsNodeRow {
112 pub node_principal: String,
113 pub node_operator_principal: String,
114 pub node_provider_principal: String,
115 pub subnet_principal: String,
116 pub subnet_kind: String,
117 pub data_center_id: String,
118}
119
120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
124pub struct NnsNodeInfoReport {
125 pub schema_version: u32,
126 pub input: String,
127 pub resolved_from: String,
128 pub network: String,
129 pub registry_canister_id: String,
130 pub registry_version: u64,
131 pub fetched_at: String,
132 pub source_endpoint: String,
133 pub fetched_by: String,
134 pub node_principal: String,
135 pub node_operator_principal: String,
136 pub node_provider_principal: String,
137 pub subnet_principal: String,
138 pub subnet_kind: String,
139 pub data_center_id: String,
140}
141
142#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
146pub struct NnsNodeRefreshReport {
147 pub schema_version: u32,
148 pub network: String,
149 pub cache_path: String,
150 pub refresh_lock_path: String,
151 pub output_path: Option<String>,
152 pub registry_canister_id: String,
153 pub registry_version: u64,
154 pub fetched_at: String,
155 pub source_endpoint: String,
156 pub fetched_by: String,
157 pub dry_run: bool,
158 pub wrote_cache: bool,
159 pub replaced_existing_cache: bool,
160 pub node_count: usize,
161}
162
163#[derive(Debug, ThisError)]
167pub enum NnsNodeHostError {
168 #[error(
169 "`canic nns node` supports only the mainnet `ic` network\n\nThe NNS node list is derived from public Internet Computer mainnet registry records.\nLocal replica NNS registry discovery is not implemented yet.\n\nTry:\n canic --network ic nns node list"
170 )]
171 UnsupportedNetwork { network: String },
172
173 #[error("node cache is missing at {}", path.display())]
174 MissingCache { path: PathBuf },
175
176 #[error("failed to read node cache at {}: {source}", path.display())]
177 ReadCache { path: PathBuf, source: io::Error },
178
179 #[error("failed to parse node cache at {}: {source}", path.display())]
180 ParseCache {
181 path: PathBuf,
182 source: serde_json::Error,
183 },
184
185 #[error("failed to serialize node cache JSON for {}: {source}", path.display())]
186 SerializeCache {
187 path: PathBuf,
188 source: serde_json::Error,
189 },
190
191 #[error("unsupported node cache schema version {version}; expected {expected}")]
192 UnsupportedCacheSchemaVersion { version: u32, expected: u32 },
193
194 #[error("cached node network mismatch: path is for {requested}, report is for {actual}")]
195 NetworkMismatch { requested: String, actual: String },
196
197 #[error("node refresh is already in progress; lock exists at {} since unix_ms={started_at_unix_ms}", path.display())]
198 RefreshAlreadyInProgress {
199 path: PathBuf,
200 started_at_unix_ms: u64,
201 },
202
203 #[error("failed to create node cache directory at {}: {source}", path.display())]
204 CreateCacheDirectory { path: PathBuf, source: io::Error },
205
206 #[error("failed to create node refresh lock at {}: {source}", path.display())]
207 CreateRefreshLock { path: PathBuf, source: io::Error },
208
209 #[error("failed to read node refresh lock at {}: {source}", path.display())]
210 ReadRefreshLock { path: PathBuf, source: io::Error },
211
212 #[error("failed to parse node refresh lock at {}: {source}", path.display())]
213 ParseRefreshLock {
214 path: PathBuf,
215 source: serde_json::Error,
216 },
217
218 #[error("failed to write node refresh lock at {}: {source}", path.display())]
219 WriteRefreshLock { path: PathBuf, source: io::Error },
220
221 #[error("failed to remove node refresh lock at {}: {source}", path.display())]
222 RemoveRefreshLock { path: PathBuf, source: io::Error },
223
224 #[error("live NNS node refresh failed: {0}")]
225 NnsQuery(#[from] RegistryFetchError),
226
227 #[error("failed to write node cache temp file at {}: {source}", path.display())]
228 WriteCacheTemp { path: PathBuf, source: io::Error },
229
230 #[error("failed to sync node cache temp file at {}: {source}", path.display())]
231 SyncCacheTemp { path: PathBuf, source: io::Error },
232
233 #[error("failed to replace node cache at {} from {}: {source}", cache_path.display(), temp_path.display())]
234 ReplaceCache {
235 temp_path: PathBuf,
236 cache_path: PathBuf,
237 source: io::Error,
238 },
239
240 #[error("failed to sync node cache directory at {}: {source}", path.display())]
241 SyncCacheDirectory { path: PathBuf, source: io::Error },
242
243 #[error("failed to write refreshed node output at {}: {source}", path.display())]
244 WriteRefreshOutput { path: PathBuf, source: io::Error },
245
246 #[error("failed to sync refreshed node output at {}: {source}", path.display())]
247 SyncRefreshOutput { path: PathBuf, source: io::Error },
248
249 #[error("node {input:?} did not match the mainnet NNS node list")]
250 NodeNotFound { input: String },
251
252 #[error("node prefix {prefix:?} is ambiguous; matches: {matches:?}")]
253 AmbiguousNodePrefix {
254 prefix: String,
255 matches: Vec<String>,
256 },
257}
258
259#[must_use]
260pub fn nns_node_cache_path(icp_root: &Path, network: &str) -> PathBuf {
261 icp_root
262 .join(".canic")
263 .join("node")
264 .join(network)
265 .join("nodes.json")
266}
267
268#[must_use]
269pub fn nns_node_refresh_lock_path(icp_root: &Path, network: &str) -> PathBuf {
270 icp_root
271 .join(".canic")
272 .join("node")
273 .join(network)
274 .join("refresh.lock")
275}
276
277pub fn build_nns_node_list_report(
278 request: &NnsNodeListRequest,
279) -> Result<NnsNodeListReport, NnsNodeHostError> {
280 build_nns_node_list_report_with_source(request, &LiveNnsNodeSource)
281}
282
283pub fn build_nns_node_info_report(
284 request: &NnsNodeInfoRequest,
285) -> Result<NnsNodeInfoReport, NnsNodeHostError> {
286 build_nns_node_info_report_with_source(request, &LiveNnsNodeSource)
287}
288
289pub fn refresh_nns_node_report(
290 request: &NnsNodeRefreshRequest,
291) -> Result<NnsNodeRefreshReport, NnsNodeHostError> {
292 refresh_nns_node_report_with_source(request, &LiveNnsNodeSource)
293}
294
295fn load_cached_nns_node_report(
296 request: &NnsNodeCacheRequest,
297) -> Result<CachedNnsNodeReport, NnsNodeHostError> {
298 enforce_mainnet_network(&request.network)?;
299 let path = nns_node_cache_path(&request.icp_root, &request.network);
300 let cached = load_json_cache(
301 LoadJsonCacheRequest {
302 path,
303 network: &request.network,
304 expected_schema_version: NNS_NODE_LIST_REPORT_SCHEMA_VERSION,
305 },
306 LoadJsonCacheErrorHandlers {
307 missing_cache: |path| NnsNodeHostError::MissingCache { path },
308 read_cache: |path, source| NnsNodeHostError::ReadCache { path, source },
309 parse_cache: |path, source| NnsNodeHostError::ParseCache { path, source },
310 unsupported_schema: |version, expected| {
311 NnsNodeHostError::UnsupportedCacheSchemaVersion { version, expected }
312 },
313 network_mismatch: |requested, actual| NnsNodeHostError::NetworkMismatch {
314 requested,
315 actual,
316 },
317 },
318 )?;
319 Ok(CachedNnsNodeReport {
320 path: cached.path,
321 report: cached.report,
322 })
323}
324
325fn build_nns_node_list_report_with_source(
326 request: &NnsNodeListRequest,
327 source: &dyn NnsNodeSource,
328) -> Result<NnsNodeListReport, NnsNodeHostError> {
329 match load_cached_nns_node_report(&request.cache) {
330 Ok(cached) => Ok(cached.report),
331 Err(NnsNodeHostError::MissingCache { .. }) => {
332 let refresh_request = NnsNodeRefreshRequest {
333 cache: request.cache.clone(),
334 source_endpoint: request.source_endpoint.clone(),
335 now_unix_secs: request.now_unix_secs,
336 lock_stale_after_seconds: DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS,
337 dry_run: false,
338 output_path: None,
339 };
340 let (report, _) = refresh_nns_node_cache_with_source(&refresh_request, source)?;
341 Ok(report)
342 }
343 Err(err) => Err(err),
344 }
345}
346
347fn build_nns_node_info_report_with_source(
348 request: &NnsNodeInfoRequest,
349 source: &dyn NnsNodeSource,
350) -> Result<NnsNodeInfoReport, NnsNodeHostError> {
351 let list_request = NnsNodeListRequest {
352 cache: request.cache.clone(),
353 source_endpoint: request.source_endpoint.clone(),
354 now_unix_secs: request.now_unix_secs,
355 };
356 let report = build_nns_node_list_report_with_source(&list_request, source)?;
357 let (node, resolved_from) = resolve_node(&report, &request.input)?;
358 Ok(NnsNodeInfoReport {
359 schema_version: NNS_NODE_INFO_REPORT_SCHEMA_VERSION,
360 input: request.input.clone(),
361 resolved_from,
362 network: report.network,
363 registry_canister_id: report.registry_canister_id,
364 registry_version: report.registry_version,
365 fetched_at: report.fetched_at,
366 source_endpoint: report.source_endpoint,
367 fetched_by: report.fetched_by,
368 node_principal: node.node_principal,
369 node_operator_principal: node.node_operator_principal,
370 node_provider_principal: node.node_provider_principal,
371 subnet_principal: node.subnet_principal,
372 subnet_kind: node.subnet_kind,
373 data_center_id: node.data_center_id,
374 })
375}
376
377fn refresh_nns_node_report_with_source(
378 request: &NnsNodeRefreshRequest,
379 source: &dyn NnsNodeSource,
380) -> Result<NnsNodeRefreshReport, NnsNodeHostError> {
381 refresh_nns_node_cache_with_source(request, source).map(|(_, report)| report)
382}
383
384fn refresh_nns_node_cache_with_source(
385 request: &NnsNodeRefreshRequest,
386 source: &dyn NnsNodeSource,
387) -> Result<(NnsNodeListReport, NnsNodeRefreshReport), NnsNodeHostError> {
388 enforce_mainnet_network(&request.cache.network)?;
389 let cache_path = nns_node_cache_path(&request.cache.icp_root, &request.cache.network);
390 let lock_path = nns_node_refresh_lock_path(&request.cache.icp_root, &request.cache.network);
391 let report = fetch_nns_node_list_report_with_source(
392 &request.cache.network,
393 &request.source_endpoint,
394 request.now_unix_secs,
395 source,
396 )?;
397 let write_result = write_json_refresh_cache(
398 RefreshCacheWriteRequest {
399 cache_path: &cache_path,
400 lock_path: &lock_path,
401 network: &request.cache.network,
402 now_unix_secs: request.now_unix_secs,
403 lock_stale_after_seconds: request.lock_stale_after_seconds,
404 dry_run: request.dry_run,
405 output_path: request.output_path.as_deref(),
406 report: &report,
407 },
408 node_cache_error,
409 |path, source| NnsNodeHostError::SerializeCache { path, source },
410 )?;
411 let refresh_report = NnsNodeRefreshReport {
412 schema_version: NNS_NODE_REFRESH_REPORT_SCHEMA_VERSION,
413 network: report.network.clone(),
414 cache_path: write_result.cache_path,
415 refresh_lock_path: write_result.refresh_lock_path,
416 output_path: write_result.output_path,
417 registry_canister_id: report.registry_canister_id.clone(),
418 registry_version: report.registry_version,
419 fetched_at: report.fetched_at.clone(),
420 source_endpoint: report.source_endpoint.clone(),
421 fetched_by: report.fetched_by.clone(),
422 dry_run: request.dry_run,
423 wrote_cache: write_result.wrote_cache,
424 replaced_existing_cache: write_result.replaced_existing_cache,
425 node_count: report.node_count,
426 };
427 Ok((report, refresh_report))
428}
429
430fn fetch_nns_node_list_report_with_source(
431 network: &str,
432 source_endpoint: &str,
433 now_unix_secs: u64,
434 source: &dyn NnsNodeSource,
435) -> Result<NnsNodeListReport, NnsNodeHostError> {
436 enforce_mainnet_network(network)?;
437 let fetched_at = format_utc_timestamp_secs(now_unix_secs);
438 let mut fetch_request = MainnetRegistryFetchRequest::new(fetched_at);
439 fetch_request.endpoint = source_endpoint.to_string();
440 let list = source.fetch_nodes(&fetch_request)?;
441 Ok(node_report_from_list(list))
442}
443
444fn node_cache_error(err: CacheFileError) -> NnsNodeHostError {
445 match err {
446 CacheFileError::CreateDirectory { path, source } => {
447 NnsNodeHostError::CreateCacheDirectory { path, source }
448 }
449 CacheFileError::CreateRefreshLock { path, source } => {
450 NnsNodeHostError::CreateRefreshLock { path, source }
451 }
452 CacheFileError::ReadRefreshLock { path, source } => {
453 NnsNodeHostError::ReadRefreshLock { path, source }
454 }
455 CacheFileError::ParseRefreshLock { path, source } => {
456 NnsNodeHostError::ParseRefreshLock { path, source }
457 }
458 CacheFileError::WriteRefreshLock { path, source } => {
459 NnsNodeHostError::WriteRefreshLock { path, source }
460 }
461 CacheFileError::RemoveRefreshLock { path, source } => {
462 NnsNodeHostError::RemoveRefreshLock { path, source }
463 }
464 CacheFileError::RefreshAlreadyInProgress {
465 path,
466 started_at_unix_ms,
467 } => NnsNodeHostError::RefreshAlreadyInProgress {
468 path,
469 started_at_unix_ms,
470 },
471 CacheFileError::WriteTemp { path, source } => {
472 NnsNodeHostError::WriteCacheTemp { path, source }
473 }
474 CacheFileError::SyncTemp { path, source } => {
475 NnsNodeHostError::SyncCacheTemp { path, source }
476 }
477 CacheFileError::Replace {
478 temp_path,
479 target_path,
480 source,
481 } => NnsNodeHostError::ReplaceCache {
482 temp_path,
483 cache_path: target_path,
484 source,
485 },
486 CacheFileError::SyncDirectory { path, source } => {
487 NnsNodeHostError::SyncCacheDirectory { path, source }
488 }
489 CacheFileError::WriteOutput { path, source } => {
490 NnsNodeHostError::WriteRefreshOutput { path, source }
491 }
492 CacheFileError::SyncOutput { path, source } => {
493 NnsNodeHostError::SyncRefreshOutput { path, source }
494 }
495 }
496}
497
498#[must_use]
499pub fn nns_node_list_report_text(report: &NnsNodeListReport) -> String {
500 let mut lines = Vec::new();
501 lines.push(format!(
502 "nodes: {} count {} fetched_at {}",
503 report.network, report.node_count, report.fetched_at
504 ));
505 if report.nodes.is_empty() {
506 lines.push("nodes: none".to_string());
507 return lines.join("\n");
508 }
509 let headers = ["NODE", "OPERATOR", "PROVIDER", "SUBNET", "KIND", "DC"];
510 let rows = report
511 .nodes
512 .iter()
513 .map(|node| {
514 [
515 compact_text(&node.node_principal, COMPACT_PRINCIPAL_CHARS),
516 compact_text(&node.node_operator_principal, COMPACT_PRINCIPAL_CHARS),
517 compact_text(&node.node_provider_principal, COMPACT_PRINCIPAL_CHARS),
518 compact_text(&node.subnet_principal, COMPACT_PRINCIPAL_CHARS),
519 node.subnet_kind.clone(),
520 text_or_dash(Some(&node.data_center_id)).to_string(),
521 ]
522 })
523 .collect::<Vec<_>>();
524 let alignments = [
525 ColumnAlign::Left,
526 ColumnAlign::Left,
527 ColumnAlign::Left,
528 ColumnAlign::Left,
529 ColumnAlign::Left,
530 ColumnAlign::Left,
531 ];
532 lines.push(render_table(&headers, &rows, &alignments));
533 lines.join("\n")
534}
535
536#[must_use]
537pub fn nns_node_list_report_verbose_text(report: &NnsNodeListReport) -> String {
538 let mut lines = Vec::new();
539 lines.push(format!("source_endpoint: {}", report.source_endpoint));
540 lines.push(format!("fetched_by: {}", report.fetched_by));
541 if report.nodes.is_empty() {
542 lines.push("nodes: none".to_string());
543 return lines.join("\n");
544 }
545 let headers = [
546 "NODE",
547 "OPERATOR",
548 "PROVIDER",
549 "SUBNET",
550 "KIND",
551 "DC",
552 "REGISTRY_VERSION",
553 "FETCHED_AT",
554 ];
555 let rows = report
556 .nodes
557 .iter()
558 .map(|node| {
559 [
560 node.node_principal.clone(),
561 node.node_operator_principal.clone(),
562 node.node_provider_principal.clone(),
563 node.subnet_principal.clone(),
564 node.subnet_kind.clone(),
565 text_or_dash(Some(&node.data_center_id)).to_string(),
566 report.registry_version.to_string(),
567 report.fetched_at.clone(),
568 ]
569 })
570 .collect::<Vec<_>>();
571 let alignments = [
572 ColumnAlign::Left,
573 ColumnAlign::Left,
574 ColumnAlign::Left,
575 ColumnAlign::Left,
576 ColumnAlign::Left,
577 ColumnAlign::Left,
578 ColumnAlign::Right,
579 ColumnAlign::Left,
580 ];
581 lines.push(render_table(&headers, &rows, &alignments));
582 lines.join("\n")
583}
584
585#[must_use]
586pub fn nns_node_info_report_text(report: &NnsNodeInfoReport) -> String {
587 [
588 format!("input: {}", report.input),
589 format!("resolved_from: {}", report.resolved_from),
590 format!("node_principal: {}", report.node_principal),
591 format!(
592 "node_operator_principal: {}",
593 report.node_operator_principal
594 ),
595 format!(
596 "node_provider_principal: {}",
597 report.node_provider_principal
598 ),
599 format!("subnet_principal: {}", report.subnet_principal),
600 format!("subnet_kind: {}", report.subnet_kind),
601 format!(
602 "data_center_id: {}",
603 text_or_dash(Some(&report.data_center_id))
604 ),
605 format!("registry_canister_id: {}", report.registry_canister_id),
606 format!("registry_version: {}", report.registry_version),
607 format!("network: {}", report.network),
608 format!("fetched_at: {}", report.fetched_at),
609 format!("source_endpoint: {}", report.source_endpoint),
610 format!("fetched_by: {}", report.fetched_by),
611 ]
612 .join("\n")
613}
614
615#[must_use]
616pub fn nns_node_refresh_report_text(report: &NnsNodeRefreshReport) -> String {
617 [
618 format!("network: {}", report.network),
619 format!("cache_path: {}", report.cache_path),
620 format!("refresh_lock_path: {}", report.refresh_lock_path),
621 format!("registry_canister_id: {}", report.registry_canister_id),
622 format!("registry_version: {}", report.registry_version),
623 format!("fetched_at: {}", report.fetched_at),
624 format!("source_endpoint: {}", report.source_endpoint),
625 format!("fetched_by: {}", report.fetched_by),
626 format!("dry_run: {}", yes_no(report.dry_run)),
627 format!("wrote_cache: {}", yes_no(report.wrote_cache)),
628 format!(
629 "replaced_existing_cache: {}",
630 yes_no(report.replaced_existing_cache)
631 ),
632 format!("node_count: {}", report.node_count),
633 ]
634 .join("\n")
635}
636
637fn node_report_from_list(list: MainnetNodeList) -> NnsNodeListReport {
638 let nodes = list
639 .nodes
640 .into_iter()
641 .map(|node| NnsNodeRow {
642 node_principal: node.principal,
643 node_operator_principal: node.node_operator_principal,
644 node_provider_principal: node.node_provider_principal,
645 subnet_principal: node.subnet_principal,
646 subnet_kind: node.subnet_kind,
647 data_center_id: node.data_center_id,
648 })
649 .collect::<Vec<_>>();
650 NnsNodeListReport {
651 schema_version: NNS_NODE_LIST_REPORT_SCHEMA_VERSION,
652 network: list.network,
653 registry_canister_id: list.registry_canister_id,
654 registry_version: list.registry_version,
655 fetched_at: list.fetched_at,
656 source_endpoint: list.source_endpoint,
657 fetched_by: list.fetched_by,
658 node_count: nodes.len(),
659 nodes,
660 }
661}
662
663trait NnsNodeSource {
667 fn fetch_nodes(
668 &self,
669 request: &MainnetRegistryFetchRequest,
670 ) -> Result<MainnetNodeList, NnsNodeHostError>;
671}
672
673struct LiveNnsNodeSource;
677
678impl NnsNodeSource for LiveNnsNodeSource {
679 fn fetch_nodes(
680 &self,
681 request: &MainnetRegistryFetchRequest,
682 ) -> Result<MainnetNodeList, NnsNodeHostError> {
683 Ok(fetch_mainnet_node_list(request)?)
684 }
685}
686
687fn enforce_mainnet_network(network: &str) -> Result<(), NnsNodeHostError> {
688 if network == MAINNET_NETWORK {
689 return Ok(());
690 }
691 Err(NnsNodeHostError::UnsupportedNetwork {
692 network: network.to_string(),
693 })
694}
695
696fn resolve_node(
697 report: &NnsNodeListReport,
698 input: &str,
699) -> Result<(NnsNodeRow, String), NnsNodeHostError> {
700 if let Ok(principal) = canonical_principal_text(input)
701 && let Some(node) = report
702 .nodes
703 .iter()
704 .find(|node| node.node_principal == principal)
705 {
706 return Ok((node.clone(), "node_principal".to_string()));
707 }
708
709 let prefix = input.trim().to_ascii_lowercase();
710 if prefix.is_empty() {
711 return Err(NnsNodeHostError::NodeNotFound {
712 input: input.to_string(),
713 });
714 }
715 let matches = report
716 .nodes
717 .iter()
718 .filter(|node| node.node_principal.starts_with(&prefix))
719 .cloned()
720 .collect::<Vec<_>>();
721 match matches.as_slice() {
722 [node] => Ok((node.clone(), "node_principal_prefix".to_string())),
723 [] => Err(NnsNodeHostError::NodeNotFound {
724 input: input.to_string(),
725 }),
726 _ => Err(NnsNodeHostError::AmbiguousNodePrefix {
727 prefix,
728 matches: matches
729 .into_iter()
730 .map(|node| node.node_principal)
731 .collect(),
732 }),
733 }
734}
735
736#[cfg(test)]
737mod tests;