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#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct NnsNodeProviderCacheRequest {
34 pub icp_root: PathBuf,
35 pub network: String,
36}
37
38#[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#[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct CachedNnsNodeProviderReport {
77 pub path: PathBuf,
78 pub report: NnsNodeProviderListReport,
79}
80
81#[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#[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#[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#[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#[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
651trait 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
670struct 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;