Skip to main content

canic_host/nns_node/
mod.rs

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///
30/// NnsNodeCacheRequest
31///
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct NnsNodeCacheRequest {
34    pub icp_root: PathBuf,
35    pub network: String,
36}
37
38///
39/// NnsNodeListRequest
40///
41#[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///
49/// NnsNodeInfoRequest
50///
51#[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///
60/// NnsNodeRefreshRequest
61///
62#[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///
73/// CachedNnsNodeReport
74///
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct CachedNnsNodeReport {
77    pub path: PathBuf,
78    pub report: NnsNodeListReport,
79}
80
81///
82/// NnsNodeListReport
83///
84#[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///
108/// NnsNodeRow
109///
110#[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///
121/// NnsNodeInfoReport
122///
123#[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///
143/// NnsNodeRefreshReport
144///
145#[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///
164/// NnsNodeHostError
165///
166#[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
663///
664/// NnsNodeSource
665///
666trait NnsNodeSource {
667    fn fetch_nodes(
668        &self,
669        request: &MainnetRegistryFetchRequest,
670    ) -> Result<MainnetNodeList, NnsNodeHostError>;
671}
672
673///
674/// LiveNnsNodeSource
675///
676struct 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;