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