Skip to main content

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