Skip to main content

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