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#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct NnsDataCenterCacheRequest {
33 pub icp_root: PathBuf,
34 pub network: String,
35}
36
37#[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#[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#[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#[derive(Clone, Debug, PartialEq)]
75pub struct CachedNnsDataCenterReport {
76 pub path: PathBuf,
77 pub report: NnsDataCenterListReport,
78}
79
80#[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#[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#[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#[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#[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
670trait NnsDataCenterSource {
674 fn fetch_data_centers(
675 &self,
676 request: &MainnetRegistryFetchRequest,
677 ) -> Result<MainnetDataCenterList, NnsDataCenterHostError>;
678}
679
680struct 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;