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, 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, MainnetNodeList, MainnetRegistryFetchRequest, RegistryFetchError,
12 fetch_mainnet_node_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_SOURCE_ENDPOINT: &str = DEFAULT_MAINNET_ENDPOINT;
23pub const DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS: u64 = 30 * 60;
24pub const NNS_NODE_LIST_REPORT_SCHEMA_VERSION: u32 = 1;
25pub const NNS_NODE_INFO_REPORT_SCHEMA_VERSION: u32 = 1;
26pub const NNS_NODE_REFRESH_REPORT_SCHEMA_VERSION: u32 = 1;
27pub const NNS_NODE_SUBNET_KIND_APPLICATION: &str = "application";
28pub const NNS_NODE_SUBNET_KIND_SYSTEM: &str = "system";
29pub const NNS_NODE_SUBNET_KIND_UNKNOWN: &str = "unknown";
30const COMPACT_PRINCIPAL_CHARS: usize = 5;
31
32#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct NnsNodeCacheRequest {
37 pub icp_root: PathBuf,
38 pub network: String,
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct NnsNodeListRequest {
46 pub cache: NnsNodeCacheRequest,
47 pub source_endpoint: String,
48 pub now_unix_secs: u64,
49 pub filters: NnsNodeListFilters,
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct NnsNodeInfoRequest {
57 pub cache: NnsNodeCacheRequest,
58 pub source_endpoint: String,
59 pub input: String,
60 pub now_unix_secs: u64,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct NnsNodeRefreshRequest {
68 pub cache: NnsNodeCacheRequest,
69 pub source_endpoint: String,
70 pub now_unix_secs: u64,
71 pub lock_stale_after_seconds: u64,
72 pub dry_run: bool,
73 pub output_path: Option<PathBuf>,
74}
75
76#[derive(Clone, Debug, Default, Eq, PartialEq)]
80pub struct NnsNodeListFilters {
81 pub subnet: Option<String>,
82 pub subnet_kind: Option<String>,
83 pub data_center: Option<String>,
84 pub node_provider: Option<String>,
85 pub node_operator: Option<String>,
86}
87
88impl NnsNodeListFilters {
89 #[must_use]
90 pub const fn is_empty(&self) -> bool {
91 self.subnet.is_none()
92 && self.subnet_kind.is_none()
93 && self.data_center.is_none()
94 && self.node_provider.is_none()
95 && self.node_operator.is_none()
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct CachedNnsNodeReport {
104 pub path: PathBuf,
105 pub report: NnsNodeListReport,
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
112pub struct NnsNodeListReport {
113 pub schema_version: u32,
114 pub network: String,
115 pub registry_canister_id: String,
116 pub registry_version: u64,
117 pub fetched_at: String,
118 pub source_endpoint: String,
119 pub fetched_by: String,
120 pub node_count: usize,
121 pub nodes: Vec<NnsNodeRow>,
122}
123
124impl JsonCacheReport for NnsNodeListReport {
125 fn schema_version(&self) -> u32 {
126 self.schema_version
127 }
128
129 fn network(&self) -> &str {
130 &self.network
131 }
132}
133
134#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
138pub struct NnsNodeRow {
139 pub node_principal: String,
140 pub node_operator_principal: String,
141 pub node_provider_principal: String,
142 pub subnet_principal: String,
143 pub subnet_kind: String,
144 pub data_center_id: String,
145}
146
147#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151pub struct NnsNodeInfoReport {
152 pub schema_version: u32,
153 pub input: String,
154 pub resolved_from: String,
155 pub network: String,
156 pub registry_canister_id: String,
157 pub registry_version: u64,
158 pub fetched_at: String,
159 pub source_endpoint: String,
160 pub fetched_by: String,
161 pub node_principal: String,
162 pub node_operator_principal: String,
163 pub node_provider_principal: String,
164 pub subnet_principal: String,
165 pub subnet_kind: String,
166 pub data_center_id: String,
167}
168
169#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
173pub struct NnsNodeRefreshReport {
174 pub schema_version: u32,
175 pub network: String,
176 pub cache_path: String,
177 pub refresh_lock_path: String,
178 pub output_path: Option<String>,
179 pub registry_canister_id: String,
180 pub registry_version: u64,
181 pub fetched_at: String,
182 pub source_endpoint: String,
183 pub fetched_by: String,
184 pub dry_run: bool,
185 pub wrote_cache: bool,
186 pub replaced_existing_cache: bool,
187 pub node_count: usize,
188}
189
190#[derive(Debug, ThisError)]
194pub enum NnsNodeHostError {
195 #[error(
196 "`canic nns node` supports only the mainnet `ic` network\n\nThe NNS node 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 list"
197 )]
198 UnsupportedNetwork { network: String },
199
200 #[error("node cache is missing at {}", path.display())]
201 MissingCache { path: PathBuf },
202
203 #[error("failed to read node cache at {}: {source}", path.display())]
204 ReadCache { path: PathBuf, source: io::Error },
205
206 #[error("failed to parse node cache at {}: {source}", path.display())]
207 ParseCache {
208 path: PathBuf,
209 source: serde_json::Error,
210 },
211
212 #[error("failed to serialize node cache JSON for {}: {source}", path.display())]
213 SerializeCache {
214 path: PathBuf,
215 source: serde_json::Error,
216 },
217
218 #[error("unsupported node cache schema version {version}; expected {expected}")]
219 UnsupportedCacheSchemaVersion { version: u32, expected: u32 },
220
221 #[error("cached node network mismatch: path is for {requested}, report is for {actual}")]
222 NetworkMismatch { requested: String, actual: String },
223
224 #[error("node refresh is already in progress; lock exists at {} since unix_ms={started_at_unix_ms}", path.display())]
225 RefreshAlreadyInProgress {
226 path: PathBuf,
227 started_at_unix_ms: u64,
228 },
229
230 #[error("failed to create node cache directory at {}: {source}", path.display())]
231 CreateCacheDirectory { path: PathBuf, source: io::Error },
232
233 #[error("failed to create node refresh lock at {}: {source}", path.display())]
234 CreateRefreshLock { path: PathBuf, source: io::Error },
235
236 #[error("failed to read node refresh lock at {}: {source}", path.display())]
237 ReadRefreshLock { path: PathBuf, source: io::Error },
238
239 #[error("failed to parse node refresh lock at {}: {source}", path.display())]
240 ParseRefreshLock {
241 path: PathBuf,
242 source: serde_json::Error,
243 },
244
245 #[error("failed to write node refresh lock at {}: {source}", path.display())]
246 WriteRefreshLock { path: PathBuf, source: io::Error },
247
248 #[error("failed to remove node refresh lock at {}: {source}", path.display())]
249 RemoveRefreshLock { path: PathBuf, source: io::Error },
250
251 #[error("live NNS node refresh failed: {0}")]
252 NnsQuery(#[from] RegistryFetchError),
253
254 #[error("failed to write node cache temp file at {}: {source}", path.display())]
255 WriteCacheTemp { path: PathBuf, source: io::Error },
256
257 #[error("failed to sync node cache temp file at {}: {source}", path.display())]
258 SyncCacheTemp { path: PathBuf, source: io::Error },
259
260 #[error("failed to replace node cache at {} from {}: {source}", cache_path.display(), temp_path.display())]
261 ReplaceCache {
262 temp_path: PathBuf,
263 cache_path: PathBuf,
264 source: io::Error,
265 },
266
267 #[error("failed to sync node cache directory at {}: {source}", path.display())]
268 SyncCacheDirectory { path: PathBuf, source: io::Error },
269
270 #[error("failed to write refreshed node output at {}: {source}", path.display())]
271 WriteRefreshOutput { path: PathBuf, source: io::Error },
272
273 #[error("failed to sync refreshed node output at {}: {source}", path.display())]
274 SyncRefreshOutput { path: PathBuf, source: io::Error },
275
276 #[error("node {input:?} did not match the mainnet NNS node list")]
277 NodeNotFound { input: String },
278
279 #[error("node prefix {prefix:?} is ambiguous; matches: {matches:?}")]
280 AmbiguousNodePrefix {
281 prefix: String,
282 matches: Vec<String>,
283 },
284}
285
286#[must_use]
287pub fn nns_node_cache_path(icp_root: &Path, network: &str) -> PathBuf {
288 icp_root
289 .join(".canic")
290 .join("node")
291 .join(network)
292 .join("nodes.json")
293}
294
295#[must_use]
296pub fn nns_node_refresh_lock_path(icp_root: &Path, network: &str) -> PathBuf {
297 icp_root
298 .join(".canic")
299 .join("node")
300 .join(network)
301 .join("refresh.lock")
302}
303
304pub fn build_nns_node_list_report(
305 request: &NnsNodeListRequest,
306) -> Result<NnsNodeListReport, NnsNodeHostError> {
307 build_nns_node_list_report_with_source(request, &LiveNnsNodeSource)
308}
309
310pub fn build_nns_node_info_report(
311 request: &NnsNodeInfoRequest,
312) -> Result<NnsNodeInfoReport, NnsNodeHostError> {
313 build_nns_node_info_report_with_source(request, &LiveNnsNodeSource)
314}
315
316pub fn refresh_nns_node_report(
317 request: &NnsNodeRefreshRequest,
318) -> Result<NnsNodeRefreshReport, NnsNodeHostError> {
319 refresh_nns_node_report_with_source(request, &LiveNnsNodeSource)
320}
321
322fn load_cached_nns_node_report(
323 request: &NnsNodeCacheRequest,
324) -> Result<CachedNnsNodeReport, NnsNodeHostError> {
325 enforce_mainnet_network(&request.network)?;
326 let path = nns_node_cache_path(&request.icp_root, &request.network);
327 let cached = load_json_cache(
328 LoadJsonCacheRequest {
329 path,
330 network: &request.network,
331 expected_schema_version: NNS_NODE_LIST_REPORT_SCHEMA_VERSION,
332 },
333 LoadJsonCacheErrorHandlers {
334 missing_cache: |path| NnsNodeHostError::MissingCache { path },
335 read_cache: |path, source| NnsNodeHostError::ReadCache { path, source },
336 parse_cache: |path, source| NnsNodeHostError::ParseCache { path, source },
337 unsupported_schema: |version, expected| {
338 NnsNodeHostError::UnsupportedCacheSchemaVersion { version, expected }
339 },
340 network_mismatch: |requested, actual| NnsNodeHostError::NetworkMismatch {
341 requested,
342 actual,
343 },
344 },
345 )?;
346 Ok(CachedNnsNodeReport {
347 path: cached.path,
348 report: cached.report,
349 })
350}
351
352fn build_nns_node_list_report_with_source(
353 request: &NnsNodeListRequest,
354 source: &dyn NnsNodeSource,
355) -> Result<NnsNodeListReport, NnsNodeHostError> {
356 let report = match load_cached_nns_node_report(&request.cache) {
357 Ok(cached) => cached.report,
358 Err(NnsNodeHostError::MissingCache { .. }) => {
359 let refresh_request = NnsNodeRefreshRequest {
360 cache: request.cache.clone(),
361 source_endpoint: request.source_endpoint.clone(),
362 now_unix_secs: request.now_unix_secs,
363 lock_stale_after_seconds: DEFAULT_NODE_REFRESH_LOCK_STALE_SECONDS,
364 dry_run: false,
365 output_path: None,
366 };
367 let (report, _) = refresh_nns_node_cache_with_source(&refresh_request, source)?;
368 report
369 }
370 Err(err) => return Err(err),
371 };
372 Ok(filter_node_list_report(report, &request.filters))
373}
374
375fn build_nns_node_info_report_with_source(
376 request: &NnsNodeInfoRequest,
377 source: &dyn NnsNodeSource,
378) -> Result<NnsNodeInfoReport, NnsNodeHostError> {
379 let list_request = NnsNodeListRequest {
380 cache: request.cache.clone(),
381 source_endpoint: request.source_endpoint.clone(),
382 now_unix_secs: request.now_unix_secs,
383 filters: NnsNodeListFilters::default(),
384 };
385 let report = build_nns_node_list_report_with_source(&list_request, source)?;
386 let (node, resolved_from) = resolve_node(&report, &request.input)?;
387 Ok(NnsNodeInfoReport {
388 schema_version: NNS_NODE_INFO_REPORT_SCHEMA_VERSION,
389 input: request.input.clone(),
390 resolved_from,
391 network: report.network,
392 registry_canister_id: report.registry_canister_id,
393 registry_version: report.registry_version,
394 fetched_at: report.fetched_at,
395 source_endpoint: report.source_endpoint,
396 fetched_by: report.fetched_by,
397 node_principal: node.node_principal,
398 node_operator_principal: node.node_operator_principal,
399 node_provider_principal: node.node_provider_principal,
400 subnet_principal: node.subnet_principal,
401 subnet_kind: node.subnet_kind,
402 data_center_id: node.data_center_id,
403 })
404}
405
406fn refresh_nns_node_report_with_source(
407 request: &NnsNodeRefreshRequest,
408 source: &dyn NnsNodeSource,
409) -> Result<NnsNodeRefreshReport, NnsNodeHostError> {
410 refresh_nns_node_cache_with_source(request, source).map(|(_, report)| report)
411}
412
413fn refresh_nns_node_cache_with_source(
414 request: &NnsNodeRefreshRequest,
415 source: &dyn NnsNodeSource,
416) -> Result<(NnsNodeListReport, NnsNodeRefreshReport), NnsNodeHostError> {
417 enforce_mainnet_network(&request.cache.network)?;
418 let cache_path = nns_node_cache_path(&request.cache.icp_root, &request.cache.network);
419 let lock_path = nns_node_refresh_lock_path(&request.cache.icp_root, &request.cache.network);
420 let report = fetch_nns_node_list_report_with_source(
421 &request.cache.network,
422 &request.source_endpoint,
423 request.now_unix_secs,
424 source,
425 )?;
426 let write_result = write_json_refresh_cache(
427 RefreshCacheWriteRequest {
428 cache_path: &cache_path,
429 lock_path: &lock_path,
430 network: &request.cache.network,
431 now_unix_secs: request.now_unix_secs,
432 lock_stale_after_seconds: request.lock_stale_after_seconds,
433 dry_run: request.dry_run,
434 output_path: request.output_path.as_deref(),
435 report: &report,
436 },
437 node_cache_error,
438 |path, source| NnsNodeHostError::SerializeCache { path, source },
439 )?;
440 let refresh_report = NnsNodeRefreshReport {
441 schema_version: NNS_NODE_REFRESH_REPORT_SCHEMA_VERSION,
442 network: report.network.clone(),
443 cache_path: write_result.cache_path,
444 refresh_lock_path: write_result.refresh_lock_path,
445 output_path: write_result.output_path,
446 registry_canister_id: report.registry_canister_id.clone(),
447 registry_version: report.registry_version,
448 fetched_at: report.fetched_at.clone(),
449 source_endpoint: report.source_endpoint.clone(),
450 fetched_by: report.fetched_by.clone(),
451 dry_run: request.dry_run,
452 wrote_cache: write_result.wrote_cache,
453 replaced_existing_cache: write_result.replaced_existing_cache,
454 node_count: report.node_count,
455 };
456 Ok((report, refresh_report))
457}
458
459fn fetch_nns_node_list_report_with_source(
460 network: &str,
461 source_endpoint: &str,
462 now_unix_secs: u64,
463 source: &dyn NnsNodeSource,
464) -> Result<NnsNodeListReport, NnsNodeHostError> {
465 enforce_mainnet_network(network)?;
466 let fetched_at = format_utc_timestamp_secs(now_unix_secs);
467 let mut fetch_request = MainnetRegistryFetchRequest::new(fetched_at);
468 fetch_request.endpoint = source_endpoint.to_string();
469 let list = source.fetch_nodes(&fetch_request)?;
470 Ok(node_report_from_list(list))
471}
472
473fn node_cache_error(err: CacheFileError) -> NnsNodeHostError {
474 match err {
475 CacheFileError::CreateDirectory { path, source } => {
476 NnsNodeHostError::CreateCacheDirectory { path, source }
477 }
478 CacheFileError::CreateRefreshLock { path, source } => {
479 NnsNodeHostError::CreateRefreshLock { path, source }
480 }
481 CacheFileError::ReadRefreshLock { path, source } => {
482 NnsNodeHostError::ReadRefreshLock { path, source }
483 }
484 CacheFileError::ParseRefreshLock { path, source } => {
485 NnsNodeHostError::ParseRefreshLock { path, source }
486 }
487 CacheFileError::WriteRefreshLock { path, source } => {
488 NnsNodeHostError::WriteRefreshLock { path, source }
489 }
490 CacheFileError::RemoveRefreshLock { path, source } => {
491 NnsNodeHostError::RemoveRefreshLock { path, source }
492 }
493 CacheFileError::RefreshAlreadyInProgress {
494 path,
495 started_at_unix_ms,
496 } => NnsNodeHostError::RefreshAlreadyInProgress {
497 path,
498 started_at_unix_ms,
499 },
500 CacheFileError::WriteTemp { path, source } => {
501 NnsNodeHostError::WriteCacheTemp { path, source }
502 }
503 CacheFileError::SyncTemp { path, source } => {
504 NnsNodeHostError::SyncCacheTemp { path, source }
505 }
506 CacheFileError::Replace {
507 temp_path,
508 target_path,
509 source,
510 } => NnsNodeHostError::ReplaceCache {
511 temp_path,
512 cache_path: target_path,
513 source,
514 },
515 CacheFileError::SyncDirectory { path, source } => {
516 NnsNodeHostError::SyncCacheDirectory { path, source }
517 }
518 CacheFileError::WriteOutput { path, source } => {
519 NnsNodeHostError::WriteRefreshOutput { path, source }
520 }
521 CacheFileError::SyncOutput { path, source } => {
522 NnsNodeHostError::SyncRefreshOutput { path, source }
523 }
524 }
525}
526
527#[must_use]
528pub fn nns_node_list_report_text(report: &NnsNodeListReport) -> String {
529 let mut lines = Vec::new();
530 lines.push(format!(
531 "nodes: {} count {} fetched_at {}",
532 report.network, report.node_count, report.fetched_at
533 ));
534 if report.nodes.is_empty() {
535 lines.push("nodes: none".to_string());
536 return lines.join("\n");
537 }
538 let headers = ["NODE", "OPERATOR", "PROVIDER", "SUBNET", "KIND", "DC"];
539 let rows = report
540 .nodes
541 .iter()
542 .map(|node| {
543 [
544 compact_text(&node.node_principal, COMPACT_PRINCIPAL_CHARS),
545 compact_text(&node.node_operator_principal, COMPACT_PRINCIPAL_CHARS),
546 compact_text(&node.node_provider_principal, COMPACT_PRINCIPAL_CHARS),
547 compact_text(&node.subnet_principal, COMPACT_PRINCIPAL_CHARS),
548 node.subnet_kind.clone(),
549 text_or_dash(Some(&node.data_center_id)).to_string(),
550 ]
551 })
552 .collect::<Vec<_>>();
553 let alignments = [
554 ColumnAlign::Left,
555 ColumnAlign::Left,
556 ColumnAlign::Left,
557 ColumnAlign::Left,
558 ColumnAlign::Left,
559 ColumnAlign::Left,
560 ];
561 lines.push(render_table(&headers, &rows, &alignments));
562 lines.join("\n")
563}
564
565#[must_use]
566pub fn nns_node_list_report_verbose_text(report: &NnsNodeListReport) -> String {
567 let mut lines = Vec::new();
568 lines.push(format!("source_endpoint: {}", report.source_endpoint));
569 lines.push(format!("fetched_by: {}", report.fetched_by));
570 if report.nodes.is_empty() {
571 lines.push("nodes: none".to_string());
572 return lines.join("\n");
573 }
574 let headers = [
575 "NODE",
576 "OPERATOR",
577 "PROVIDER",
578 "SUBNET",
579 "KIND",
580 "DC",
581 "REGISTRY_VERSION",
582 "FETCHED_AT",
583 ];
584 let rows = report
585 .nodes
586 .iter()
587 .map(|node| {
588 [
589 node.node_principal.clone(),
590 node.node_operator_principal.clone(),
591 node.node_provider_principal.clone(),
592 node.subnet_principal.clone(),
593 node.subnet_kind.clone(),
594 text_or_dash(Some(&node.data_center_id)).to_string(),
595 report.registry_version.to_string(),
596 report.fetched_at.clone(),
597 ]
598 })
599 .collect::<Vec<_>>();
600 let alignments = [
601 ColumnAlign::Left,
602 ColumnAlign::Left,
603 ColumnAlign::Left,
604 ColumnAlign::Left,
605 ColumnAlign::Left,
606 ColumnAlign::Left,
607 ColumnAlign::Right,
608 ColumnAlign::Left,
609 ];
610 lines.push(render_table(&headers, &rows, &alignments));
611 lines.join("\n")
612}
613
614#[must_use]
615pub fn nns_node_info_report_text(report: &NnsNodeInfoReport) -> String {
616 [
617 format!("input: {}", report.input),
618 format!("resolved_from: {}", report.resolved_from),
619 format!("node_principal: {}", report.node_principal),
620 format!(
621 "node_operator_principal: {}",
622 report.node_operator_principal
623 ),
624 format!(
625 "node_provider_principal: {}",
626 report.node_provider_principal
627 ),
628 format!("subnet_principal: {}", report.subnet_principal),
629 format!("subnet_kind: {}", report.subnet_kind),
630 format!(
631 "data_center_id: {}",
632 text_or_dash(Some(&report.data_center_id))
633 ),
634 format!("registry_canister_id: {}", report.registry_canister_id),
635 format!("registry_version: {}", report.registry_version),
636 format!("network: {}", report.network),
637 format!("fetched_at: {}", report.fetched_at),
638 format!("source_endpoint: {}", report.source_endpoint),
639 format!("fetched_by: {}", report.fetched_by),
640 ]
641 .join("\n")
642}
643
644#[must_use]
645pub fn nns_node_refresh_report_text(report: &NnsNodeRefreshReport) -> String {
646 [
647 format!("network: {}", report.network),
648 format!("cache_path: {}", report.cache_path),
649 format!("refresh_lock_path: {}", report.refresh_lock_path),
650 format!("registry_canister_id: {}", report.registry_canister_id),
651 format!("registry_version: {}", report.registry_version),
652 format!("fetched_at: {}", report.fetched_at),
653 format!("source_endpoint: {}", report.source_endpoint),
654 format!("fetched_by: {}", report.fetched_by),
655 format!("dry_run: {}", yes_no(report.dry_run)),
656 format!("wrote_cache: {}", yes_no(report.wrote_cache)),
657 format!(
658 "replaced_existing_cache: {}",
659 yes_no(report.replaced_existing_cache)
660 ),
661 format!("node_count: {}", report.node_count),
662 ]
663 .join("\n")
664}
665
666fn node_report_from_list(list: MainnetNodeList) -> NnsNodeListReport {
667 let nodes = list
668 .nodes
669 .into_iter()
670 .map(|node| NnsNodeRow {
671 node_principal: node.principal,
672 node_operator_principal: node.node_operator_principal,
673 node_provider_principal: node.node_provider_principal,
674 subnet_principal: node.subnet_principal,
675 subnet_kind: node.subnet_kind,
676 data_center_id: node.data_center_id,
677 })
678 .collect::<Vec<_>>();
679 NnsNodeListReport {
680 schema_version: NNS_NODE_LIST_REPORT_SCHEMA_VERSION,
681 network: list.network,
682 registry_canister_id: list.registry_canister_id,
683 registry_version: list.registry_version,
684 fetched_at: list.fetched_at,
685 source_endpoint: list.source_endpoint,
686 fetched_by: list.fetched_by,
687 node_count: nodes.len(),
688 nodes,
689 }
690}
691
692fn filter_node_list_report(
693 mut report: NnsNodeListReport,
694 filters: &NnsNodeListFilters,
695) -> NnsNodeListReport {
696 if filters.is_empty() {
697 return report;
698 }
699 report
700 .nodes
701 .retain(|node| node_matches_filters(node, filters));
702 report.node_count = report.nodes.len();
703 report
704}
705
706fn node_matches_filters(node: &NnsNodeRow, filters: &NnsNodeListFilters) -> bool {
707 filters
708 .subnet
709 .as_deref()
710 .is_none_or(|filter| principal_filter_matches(&node.subnet_principal, filter))
711 && filters
712 .subnet_kind
713 .as_deref()
714 .is_none_or(|filter| text_filter_equals(&node.subnet_kind, filter))
715 && filters
716 .data_center
717 .as_deref()
718 .is_none_or(|filter| text_filter_starts_with(&node.data_center_id, filter))
719 && filters
720 .node_provider
721 .as_deref()
722 .is_none_or(|filter| principal_filter_matches(&node.node_provider_principal, filter))
723 && filters
724 .node_operator
725 .as_deref()
726 .is_none_or(|filter| principal_filter_matches(&node.node_operator_principal, filter))
727}
728
729fn principal_filter_matches(value: &str, filter: &str) -> bool {
730 let Some(filter) = non_empty_filter(filter) else {
731 return false;
732 };
733 if let Ok(principal) = canonical_principal_text(filter) {
734 value == principal
735 } else {
736 value.starts_with(&filter.to_ascii_lowercase())
737 }
738}
739
740fn text_filter_starts_with(value: &str, filter: &str) -> bool {
741 let Some(filter) = non_empty_filter(filter) else {
742 return false;
743 };
744 value
745 .to_ascii_lowercase()
746 .starts_with(&filter.to_ascii_lowercase())
747}
748
749fn text_filter_equals(value: &str, filter: &str) -> bool {
750 let Some(filter) = non_empty_filter(filter) else {
751 return false;
752 };
753 value.eq_ignore_ascii_case(filter)
754}
755
756fn non_empty_filter(filter: &str) -> Option<&str> {
757 let filter = filter.trim();
758 (!filter.is_empty()).then_some(filter)
759}
760
761trait NnsNodeSource {
765 fn fetch_nodes(
766 &self,
767 request: &MainnetRegistryFetchRequest,
768 ) -> Result<MainnetNodeList, NnsNodeHostError>;
769}
770
771struct LiveNnsNodeSource;
775
776impl NnsNodeSource for LiveNnsNodeSource {
777 fn fetch_nodes(
778 &self,
779 request: &MainnetRegistryFetchRequest,
780 ) -> Result<MainnetNodeList, NnsNodeHostError> {
781 Ok(fetch_mainnet_node_list(request)?)
782 }
783}
784
785fn enforce_mainnet_network(network: &str) -> Result<(), NnsNodeHostError> {
786 if network == MAINNET_NETWORK {
787 return Ok(());
788 }
789 Err(NnsNodeHostError::UnsupportedNetwork {
790 network: network.to_string(),
791 })
792}
793
794fn resolve_node(
795 report: &NnsNodeListReport,
796 input: &str,
797) -> Result<(NnsNodeRow, String), NnsNodeHostError> {
798 if let Ok(principal) = canonical_principal_text(input)
799 && let Some(node) = report
800 .nodes
801 .iter()
802 .find(|node| node.node_principal == principal)
803 {
804 return Ok((node.clone(), "node_principal".to_string()));
805 }
806
807 let prefix = input.trim().to_ascii_lowercase();
808 if prefix.is_empty() {
809 return Err(NnsNodeHostError::NodeNotFound {
810 input: input.to_string(),
811 });
812 }
813 let matches = report
814 .nodes
815 .iter()
816 .filter(|node| node.node_principal.starts_with(&prefix))
817 .cloned()
818 .collect::<Vec<_>>();
819 match matches.as_slice() {
820 [node] => Ok((node.clone(), "node_principal_prefix".to_string())),
821 [] => Err(NnsNodeHostError::NodeNotFound {
822 input: input.to_string(),
823 }),
824 _ => Err(NnsNodeHostError::AmbiguousNodePrefix {
825 prefix,
826 matches: matches
827 .into_iter()
828 .map(|node| node.node_principal)
829 .collect(),
830 }),
831 }
832}
833
834#[cfg(test)]
835mod tests;