Skip to main content

canic_host/nns_node_operator/
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, optional_node_count_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, MainnetNodeOperatorList, MainnetRegistryFetchRequest,
12    RegistryFetchError, fetch_mainnet_node_operator_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_OPERATOR_SOURCE_ENDPOINT: &str = DEFAULT_MAINNET_ENDPOINT;
23pub const DEFAULT_NODE_OPERATOR_REFRESH_LOCK_STALE_SECONDS: u64 = 30 * 60;
24pub const NNS_NODE_OPERATOR_LIST_REPORT_SCHEMA_VERSION: u32 = 1;
25pub const NNS_NODE_OPERATOR_INFO_REPORT_SCHEMA_VERSION: u32 = 1;
26pub const NNS_NODE_OPERATOR_REFRESH_REPORT_SCHEMA_VERSION: u32 = 1;
27const COMPACT_PRINCIPAL_CHARS: usize = 5;
28
29///
30/// NnsNodeOperatorCacheRequest
31///
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct NnsNodeOperatorCacheRequest {
34    pub icp_root: PathBuf,
35    pub network: String,
36}
37
38///
39/// NnsNodeOperatorListRequest
40///
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct NnsNodeOperatorListRequest {
43    pub cache: NnsNodeOperatorCacheRequest,
44    pub source_endpoint: String,
45    pub now_unix_secs: u64,
46}
47
48///
49/// NnsNodeOperatorInfoRequest
50///
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct NnsNodeOperatorInfoRequest {
53    pub cache: NnsNodeOperatorCacheRequest,
54    pub source_endpoint: String,
55    pub input: String,
56    pub now_unix_secs: u64,
57}
58
59///
60/// NnsNodeOperatorRefreshRequest
61///
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct NnsNodeOperatorRefreshRequest {
64    pub cache: NnsNodeOperatorCacheRequest,
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/// CachedNnsNodeOperatorReport
74///
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct CachedNnsNodeOperatorReport {
77    pub path: PathBuf,
78    pub report: NnsNodeOperatorListReport,
79}
80
81///
82/// NnsNodeOperatorListReport
83///
84#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
85pub struct NnsNodeOperatorListReport {
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_operator_count: usize,
94    pub node_operators: Vec<NnsNodeOperatorRow>,
95}
96
97impl JsonCacheReport for NnsNodeOperatorListReport {
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/// NnsNodeOperatorRow
109///
110#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
111pub struct NnsNodeOperatorRow {
112    pub node_operator_principal: String,
113    pub node_provider_principal: String,
114    pub node_allowance: u64,
115    pub data_center_id: String,
116    pub node_count: Option<u32>,
117}
118
119///
120/// NnsNodeOperatorInfoReport
121///
122#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
123pub struct NnsNodeOperatorInfoReport {
124    pub schema_version: u32,
125    pub input: String,
126    pub resolved_from: String,
127    pub network: String,
128    pub registry_canister_id: String,
129    pub registry_version: u64,
130    pub fetched_at: String,
131    pub source_endpoint: String,
132    pub fetched_by: String,
133    pub node_operator_principal: String,
134    pub node_provider_principal: String,
135    pub node_allowance: u64,
136    pub data_center_id: String,
137    pub node_count: Option<u32>,
138}
139
140///
141/// NnsNodeOperatorRefreshReport
142///
143#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
144pub struct NnsNodeOperatorRefreshReport {
145    pub schema_version: u32,
146    pub network: String,
147    pub cache_path: String,
148    pub refresh_lock_path: String,
149    pub output_path: Option<String>,
150    pub registry_canister_id: String,
151    pub registry_version: u64,
152    pub fetched_at: String,
153    pub source_endpoint: String,
154    pub fetched_by: String,
155    pub dry_run: bool,
156    pub wrote_cache: bool,
157    pub replaced_existing_cache: bool,
158    pub node_operator_count: usize,
159}
160
161///
162/// NnsNodeOperatorHostError
163///
164#[derive(Debug, ThisError)]
165pub enum NnsNodeOperatorHostError {
166    #[error(
167        "`canic nns node-operator` supports only the mainnet `ic` network\n\nThe NNS node-operator 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-operator list"
168    )]
169    UnsupportedNetwork { network: String },
170
171    #[error("node-operator cache is missing at {}", path.display())]
172    MissingCache { path: PathBuf },
173
174    #[error("failed to read node-operator cache at {}: {source}", path.display())]
175    ReadCache { path: PathBuf, source: io::Error },
176
177    #[error("failed to parse node-operator cache at {}: {source}", path.display())]
178    ParseCache {
179        path: PathBuf,
180        source: serde_json::Error,
181    },
182
183    #[error("failed to serialize node-operator cache JSON for {}: {source}", path.display())]
184    SerializeCache {
185        path: PathBuf,
186        source: serde_json::Error,
187    },
188
189    #[error("unsupported node-operator cache schema version {version}; expected {expected}")]
190    UnsupportedCacheSchemaVersion { version: u32, expected: u32 },
191
192    #[error(
193        "cached node-operator network mismatch: path is for {requested}, report is for {actual}"
194    )]
195    NetworkMismatch { requested: String, actual: String },
196
197    #[error("node-operator 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-operator cache directory at {}: {source}", path.display())]
204    CreateCacheDirectory { path: PathBuf, source: io::Error },
205
206    #[error("failed to create node-operator refresh lock at {}: {source}", path.display())]
207    CreateRefreshLock { path: PathBuf, source: io::Error },
208
209    #[error("failed to read node-operator refresh lock at {}: {source}", path.display())]
210    ReadRefreshLock { path: PathBuf, source: io::Error },
211
212    #[error("failed to parse node-operator refresh lock at {}: {source}", path.display())]
213    ParseRefreshLock {
214        path: PathBuf,
215        source: serde_json::Error,
216    },
217
218    #[error("failed to write node-operator refresh lock at {}: {source}", path.display())]
219    WriteRefreshLock { path: PathBuf, source: io::Error },
220
221    #[error("failed to remove node-operator refresh lock at {}: {source}", path.display())]
222    RemoveRefreshLock { path: PathBuf, source: io::Error },
223
224    #[error("live NNS node-operator refresh failed: {0}")]
225    NnsQuery(#[from] RegistryFetchError),
226
227    #[error("failed to write node-operator cache temp file at {}: {source}", path.display())]
228    WriteCacheTemp { path: PathBuf, source: io::Error },
229
230    #[error("failed to sync node-operator cache temp file at {}: {source}", path.display())]
231    SyncCacheTemp { path: PathBuf, source: io::Error },
232
233    #[error("failed to replace node-operator 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-operator cache directory at {}: {source}", path.display())]
241    SyncCacheDirectory { path: PathBuf, source: io::Error },
242
243    #[error("failed to write refreshed node-operator output at {}: {source}", path.display())]
244    WriteRefreshOutput { path: PathBuf, source: io::Error },
245
246    #[error("failed to sync refreshed node-operator output at {}: {source}", path.display())]
247    SyncRefreshOutput { path: PathBuf, source: io::Error },
248
249    #[error("node operator {input:?} did not match the mainnet NNS node-operator list")]
250    NodeOperatorNotFound { input: String },
251
252    #[error("node-operator prefix {prefix:?} is ambiguous; matches: {matches:?}")]
253    AmbiguousNodeOperatorPrefix {
254        prefix: String,
255        matches: Vec<String>,
256    },
257}
258
259#[must_use]
260pub fn nns_node_operator_cache_path(icp_root: &Path, network: &str) -> PathBuf {
261    icp_root
262        .join(".canic")
263        .join("node-operator")
264        .join(network)
265        .join("operators.json")
266}
267
268#[must_use]
269pub fn nns_node_operator_refresh_lock_path(icp_root: &Path, network: &str) -> PathBuf {
270    icp_root
271        .join(".canic")
272        .join("node-operator")
273        .join(network)
274        .join("refresh.lock")
275}
276
277pub fn load_cached_nns_node_operator_report(
278    request: &NnsNodeOperatorCacheRequest,
279) -> Result<CachedNnsNodeOperatorReport, NnsNodeOperatorHostError> {
280    enforce_mainnet_network(&request.network)?;
281    let path = nns_node_operator_cache_path(&request.icp_root, &request.network);
282    let cached = load_json_cache(
283        LoadJsonCacheRequest {
284            path,
285            network: &request.network,
286            expected_schema_version: NNS_NODE_OPERATOR_LIST_REPORT_SCHEMA_VERSION,
287        },
288        LoadJsonCacheErrorHandlers {
289            missing_cache: |path| NnsNodeOperatorHostError::MissingCache { path },
290            read_cache: |path, source| NnsNodeOperatorHostError::ReadCache { path, source },
291            parse_cache: |path, source| NnsNodeOperatorHostError::ParseCache { path, source },
292            unsupported_schema: |version, expected| {
293                NnsNodeOperatorHostError::UnsupportedCacheSchemaVersion { version, expected }
294            },
295            network_mismatch: |requested, actual| NnsNodeOperatorHostError::NetworkMismatch {
296                requested,
297                actual,
298            },
299        },
300    )?;
301    Ok(CachedNnsNodeOperatorReport {
302        path: cached.path,
303        report: cached.report,
304    })
305}
306
307pub fn build_nns_node_operator_list_report(
308    request: &NnsNodeOperatorListRequest,
309) -> Result<NnsNodeOperatorListReport, NnsNodeOperatorHostError> {
310    build_nns_node_operator_list_report_with_source(request, &LiveNnsNodeOperatorSource)
311}
312
313pub fn build_nns_node_operator_info_report(
314    request: &NnsNodeOperatorInfoRequest,
315) -> Result<NnsNodeOperatorInfoReport, NnsNodeOperatorHostError> {
316    build_nns_node_operator_info_report_with_source(request, &LiveNnsNodeOperatorSource)
317}
318
319pub fn refresh_nns_node_operator_report(
320    request: &NnsNodeOperatorRefreshRequest,
321) -> Result<NnsNodeOperatorRefreshReport, NnsNodeOperatorHostError> {
322    refresh_nns_node_operator_report_with_source(request, &LiveNnsNodeOperatorSource)
323}
324
325fn build_nns_node_operator_list_report_with_source(
326    request: &NnsNodeOperatorListRequest,
327    source: &dyn NnsNodeOperatorSource,
328) -> Result<NnsNodeOperatorListReport, NnsNodeOperatorHostError> {
329    match load_cached_nns_node_operator_report(&request.cache) {
330        Ok(cached) => Ok(cached.report),
331        Err(NnsNodeOperatorHostError::MissingCache { .. }) => {
332            let refresh_request = NnsNodeOperatorRefreshRequest {
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_OPERATOR_REFRESH_LOCK_STALE_SECONDS,
337                dry_run: false,
338                output_path: None,
339            };
340            let (report, _) =
341                refresh_nns_node_operator_cache_with_source(&refresh_request, source)?;
342            Ok(report)
343        }
344        Err(err) => Err(err),
345    }
346}
347
348fn build_nns_node_operator_info_report_with_source(
349    request: &NnsNodeOperatorInfoRequest,
350    source: &dyn NnsNodeOperatorSource,
351) -> Result<NnsNodeOperatorInfoReport, NnsNodeOperatorHostError> {
352    let list_request = NnsNodeOperatorListRequest {
353        cache: request.cache.clone(),
354        source_endpoint: request.source_endpoint.clone(),
355        now_unix_secs: request.now_unix_secs,
356    };
357    let report = build_nns_node_operator_list_report_with_source(&list_request, source)?;
358    let (operator, resolved_from) = resolve_node_operator(&report, &request.input)?;
359    Ok(NnsNodeOperatorInfoReport {
360        schema_version: NNS_NODE_OPERATOR_INFO_REPORT_SCHEMA_VERSION,
361        input: request.input.clone(),
362        resolved_from,
363        network: report.network,
364        registry_canister_id: report.registry_canister_id,
365        registry_version: report.registry_version,
366        fetched_at: report.fetched_at,
367        source_endpoint: report.source_endpoint,
368        fetched_by: report.fetched_by,
369        node_operator_principal: operator.node_operator_principal,
370        node_provider_principal: operator.node_provider_principal,
371        node_allowance: operator.node_allowance,
372        data_center_id: operator.data_center_id,
373        node_count: operator.node_count,
374    })
375}
376
377fn refresh_nns_node_operator_report_with_source(
378    request: &NnsNodeOperatorRefreshRequest,
379    source: &dyn NnsNodeOperatorSource,
380) -> Result<NnsNodeOperatorRefreshReport, NnsNodeOperatorHostError> {
381    refresh_nns_node_operator_cache_with_source(request, source).map(|(_, report)| report)
382}
383
384fn refresh_nns_node_operator_cache_with_source(
385    request: &NnsNodeOperatorRefreshRequest,
386    source: &dyn NnsNodeOperatorSource,
387) -> Result<(NnsNodeOperatorListReport, NnsNodeOperatorRefreshReport), NnsNodeOperatorHostError> {
388    enforce_mainnet_network(&request.cache.network)?;
389    let cache_path = nns_node_operator_cache_path(&request.cache.icp_root, &request.cache.network);
390    let lock_path =
391        nns_node_operator_refresh_lock_path(&request.cache.icp_root, &request.cache.network);
392    let report = fetch_nns_node_operator_list_report_with_source(
393        &request.cache.network,
394        &request.source_endpoint,
395        request.now_unix_secs,
396        source,
397    )?;
398    let write_result = write_json_refresh_cache(
399        RefreshCacheWriteRequest {
400            cache_path: &cache_path,
401            lock_path: &lock_path,
402            network: &request.cache.network,
403            now_unix_secs: request.now_unix_secs,
404            lock_stale_after_seconds: request.lock_stale_after_seconds,
405            dry_run: request.dry_run,
406            output_path: request.output_path.as_deref(),
407            report: &report,
408        },
409        node_operator_cache_error,
410        |path, source| NnsNodeOperatorHostError::SerializeCache { path, source },
411    )?;
412    let refresh_report = NnsNodeOperatorRefreshReport {
413        schema_version: NNS_NODE_OPERATOR_REFRESH_REPORT_SCHEMA_VERSION,
414        network: report.network.clone(),
415        cache_path: write_result.cache_path,
416        refresh_lock_path: write_result.refresh_lock_path,
417        output_path: write_result.output_path,
418        registry_canister_id: report.registry_canister_id.clone(),
419        registry_version: report.registry_version,
420        fetched_at: report.fetched_at.clone(),
421        source_endpoint: report.source_endpoint.clone(),
422        fetched_by: report.fetched_by.clone(),
423        dry_run: request.dry_run,
424        wrote_cache: write_result.wrote_cache,
425        replaced_existing_cache: write_result.replaced_existing_cache,
426        node_operator_count: report.node_operator_count,
427    };
428    Ok((report, refresh_report))
429}
430
431fn fetch_nns_node_operator_list_report_with_source(
432    network: &str,
433    source_endpoint: &str,
434    now_unix_secs: u64,
435    source: &dyn NnsNodeOperatorSource,
436) -> Result<NnsNodeOperatorListReport, NnsNodeOperatorHostError> {
437    enforce_mainnet_network(network)?;
438    let fetched_at = format_utc_timestamp_secs(now_unix_secs);
439    let mut fetch_request = MainnetRegistryFetchRequest::new(fetched_at);
440    fetch_request.endpoint = source_endpoint.to_string();
441    let list = source.fetch_node_operators(&fetch_request)?;
442    Ok(node_operator_report_from_list(list))
443}
444
445fn node_operator_cache_error(err: CacheFileError) -> NnsNodeOperatorHostError {
446    match err {
447        CacheFileError::CreateDirectory { path, source } => {
448            NnsNodeOperatorHostError::CreateCacheDirectory { path, source }
449        }
450        CacheFileError::CreateRefreshLock { path, source } => {
451            NnsNodeOperatorHostError::CreateRefreshLock { path, source }
452        }
453        CacheFileError::ReadRefreshLock { path, source } => {
454            NnsNodeOperatorHostError::ReadRefreshLock { path, source }
455        }
456        CacheFileError::ParseRefreshLock { path, source } => {
457            NnsNodeOperatorHostError::ParseRefreshLock { path, source }
458        }
459        CacheFileError::WriteRefreshLock { path, source } => {
460            NnsNodeOperatorHostError::WriteRefreshLock { path, source }
461        }
462        CacheFileError::RemoveRefreshLock { path, source } => {
463            NnsNodeOperatorHostError::RemoveRefreshLock { path, source }
464        }
465        CacheFileError::RefreshAlreadyInProgress {
466            path,
467            started_at_unix_ms,
468        } => NnsNodeOperatorHostError::RefreshAlreadyInProgress {
469            path,
470            started_at_unix_ms,
471        },
472        CacheFileError::WriteTemp { path, source } => {
473            NnsNodeOperatorHostError::WriteCacheTemp { path, source }
474        }
475        CacheFileError::SyncTemp { path, source } => {
476            NnsNodeOperatorHostError::SyncCacheTemp { path, source }
477        }
478        CacheFileError::Replace {
479            temp_path,
480            target_path,
481            source,
482        } => NnsNodeOperatorHostError::ReplaceCache {
483            temp_path,
484            cache_path: target_path,
485            source,
486        },
487        CacheFileError::SyncDirectory { path, source } => {
488            NnsNodeOperatorHostError::SyncCacheDirectory { path, source }
489        }
490        CacheFileError::WriteOutput { path, source } => {
491            NnsNodeOperatorHostError::WriteRefreshOutput { path, source }
492        }
493        CacheFileError::SyncOutput { path, source } => {
494            NnsNodeOperatorHostError::SyncRefreshOutput { path, source }
495        }
496    }
497}
498
499#[must_use]
500pub fn nns_node_operator_list_report_text(report: &NnsNodeOperatorListReport) -> String {
501    let mut lines = Vec::new();
502    lines.push(format!(
503        "node_operators: {} count {} fetched_at {}",
504        report.network, report.node_operator_count, report.fetched_at
505    ));
506    if report.node_operators.is_empty() {
507        lines.push("node operators: none".to_string());
508        return lines.join("\n");
509    }
510
511    let headers = ["NODE_OPERATOR", "PROVIDER", "NODES", "ALLOWANCE", "DC"];
512    let rows = report
513        .node_operators
514        .iter()
515        .map(|operator| {
516            [
517                compact_text(&operator.node_operator_principal, COMPACT_PRINCIPAL_CHARS),
518                compact_text(&operator.node_provider_principal, COMPACT_PRINCIPAL_CHARS),
519                optional_node_count_text(operator.node_count),
520                operator.node_allowance.to_string(),
521                text_or_dash(Some(&operator.data_center_id)).to_string(),
522            ]
523        })
524        .collect::<Vec<_>>();
525    let alignments = [
526        ColumnAlign::Left,
527        ColumnAlign::Left,
528        ColumnAlign::Right,
529        ColumnAlign::Right,
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_operator_list_report_verbose_text(report: &NnsNodeOperatorListReport) -> 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.node_operators.is_empty() {
542        lines.push("node operators: none".to_string());
543        return lines.join("\n");
544    }
545
546    let headers = [
547        "NODE_OPERATOR",
548        "PROVIDER",
549        "NODES",
550        "ALLOWANCE",
551        "DC",
552        "REGISTRY_VERSION",
553        "FETCHED_AT",
554    ];
555    let rows = report
556        .node_operators
557        .iter()
558        .map(|operator| {
559            [
560                operator.node_operator_principal.clone(),
561                operator.node_provider_principal.clone(),
562                optional_node_count_text(operator.node_count),
563                operator.node_allowance.to_string(),
564                text_or_dash(Some(&operator.data_center_id)).to_string(),
565                report.registry_version.to_string(),
566                report.fetched_at.clone(),
567            ]
568        })
569        .collect::<Vec<_>>();
570    let alignments = [
571        ColumnAlign::Left,
572        ColumnAlign::Left,
573        ColumnAlign::Right,
574        ColumnAlign::Right,
575        ColumnAlign::Left,
576        ColumnAlign::Right,
577        ColumnAlign::Left,
578    ];
579    lines.push(render_table(&headers, &rows, &alignments));
580    lines.join("\n")
581}
582
583#[must_use]
584pub fn nns_node_operator_info_report_text(report: &NnsNodeOperatorInfoReport) -> String {
585    [
586        format!("input: {}", report.input),
587        format!("resolved_from: {}", report.resolved_from),
588        format!(
589            "node_operator_principal: {}",
590            report.node_operator_principal
591        ),
592        format!(
593            "node_provider_principal: {}",
594            report.node_provider_principal
595        ),
596        format!(
597            "node_count: {}",
598            optional_node_count_text(report.node_count)
599        ),
600        format!("node_allowance: {}", report.node_allowance),
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_operator_refresh_report_text(report: &NnsNodeOperatorRefreshReport) -> 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_operator_count: {}", report.node_operator_count),
633    ]
634    .join("\n")
635}
636
637fn node_operator_report_from_list(list: MainnetNodeOperatorList) -> NnsNodeOperatorListReport {
638    let node_operators = list
639        .node_operators
640        .into_iter()
641        .map(|operator| NnsNodeOperatorRow {
642            node_operator_principal: operator.principal,
643            node_provider_principal: operator.node_provider_principal,
644            node_allowance: operator.node_allowance,
645            data_center_id: operator.data_center_id,
646            node_count: operator.node_count,
647        })
648        .collect::<Vec<_>>();
649    NnsNodeOperatorListReport {
650        schema_version: NNS_NODE_OPERATOR_LIST_REPORT_SCHEMA_VERSION,
651        network: list.network,
652        registry_canister_id: list.registry_canister_id,
653        registry_version: list.registry_version,
654        fetched_at: list.fetched_at,
655        source_endpoint: list.source_endpoint,
656        fetched_by: list.fetched_by,
657        node_operator_count: node_operators.len(),
658        node_operators,
659    }
660}
661
662///
663/// NnsNodeOperatorSource
664///
665trait NnsNodeOperatorSource {
666    fn fetch_node_operators(
667        &self,
668        request: &MainnetRegistryFetchRequest,
669    ) -> Result<MainnetNodeOperatorList, NnsNodeOperatorHostError>;
670}
671
672fn enforce_mainnet_network(network: &str) -> Result<(), NnsNodeOperatorHostError> {
673    if network == MAINNET_NETWORK {
674        return Ok(());
675    }
676    Err(NnsNodeOperatorHostError::UnsupportedNetwork {
677        network: network.to_string(),
678    })
679}
680
681///
682/// LiveNnsNodeOperatorSource
683///
684struct LiveNnsNodeOperatorSource;
685
686impl NnsNodeOperatorSource for LiveNnsNodeOperatorSource {
687    fn fetch_node_operators(
688        &self,
689        request: &MainnetRegistryFetchRequest,
690    ) -> Result<MainnetNodeOperatorList, NnsNodeOperatorHostError> {
691        Ok(fetch_mainnet_node_operator_list(request)?)
692    }
693}
694
695fn resolve_node_operator(
696    report: &NnsNodeOperatorListReport,
697    input: &str,
698) -> Result<(NnsNodeOperatorRow, String), NnsNodeOperatorHostError> {
699    if let Ok(principal) = canonical_principal_text(input)
700        && let Some(operator) = report
701            .node_operators
702            .iter()
703            .find(|operator| operator.node_operator_principal == principal)
704    {
705        return Ok((operator.clone(), "node_operator_principal".to_string()));
706    }
707
708    let prefix = input.trim().to_ascii_lowercase();
709    if prefix.is_empty() {
710        return Err(NnsNodeOperatorHostError::NodeOperatorNotFound {
711            input: input.to_string(),
712        });
713    }
714    let matches = report
715        .node_operators
716        .iter()
717        .filter(|operator| operator.node_operator_principal.starts_with(&prefix))
718        .cloned()
719        .collect::<Vec<_>>();
720    match matches.as_slice() {
721        [operator] => Ok((
722            operator.clone(),
723            "node_operator_principal_prefix".to_string(),
724        )),
725        [] => Err(NnsNodeOperatorHostError::NodeOperatorNotFound {
726            input: input.to_string(),
727        }),
728        _ => Err(NnsNodeOperatorHostError::AmbiguousNodeOperatorPrefix {
729            prefix,
730            matches: matches
731                .into_iter()
732                .map(|operator| operator.node_operator_principal)
733                .collect(),
734        }),
735    }
736}
737
738#[cfg(test)]
739mod tests;