Skip to main content

ic_query/subnet_catalog/
mod.rs

1mod host;
2mod model;
3mod report;
4mod resolver;
5mod text;
6mod time;
7
8use crate::ic_registry::DEFAULT_MAINNET_ENDPOINT;
9use candid::Principal;
10pub(crate) use host::{
11    LiveNnsRegistryRefreshSource, SubnetCatalogCacheRequest, SubnetCatalogHostError,
12    SubnetCatalogRefreshRequest, SubnetCatalogRefreshSource, load_or_refresh_subnet_catalog,
13    refresh_subnet_catalog,
14};
15#[cfg(test)]
16pub(crate) use host::{
17    load_cached_subnet_catalog, refresh_subnet_catalog_with_source, subnet_catalog_path,
18    subnet_catalog_refresh_lock_path,
19};
20pub use model::{
21    ClassificationSource, GeographicScope, RoutingRange, SubnetCatalog, SubnetInfo, SubnetKind,
22    SubnetSpecialization,
23};
24pub(crate) use report::{
25    CatalogStaleStatus, SubnetCatalogFilters, SubnetCatalogInfoReport, SubnetCatalogInfoRequest,
26    SubnetCatalogListReport, SubnetCatalogListRequest, SubnetCatalogRefreshReport,
27    build_subnet_catalog_info_report, build_subnet_catalog_list_report,
28};
29#[cfg(test)]
30pub(crate) use report::{SubnetCatalogSubnetRow, build_subnet_catalog_list_report_with_source};
31pub use resolver::{ResolveAs, ResolvedSubnet, ResolvedSubnetSubject};
32#[cfg(test)]
33pub(crate) use text::compact_principal;
34pub(crate) use text::{
35    subnet_catalog_info_report_text, subnet_catalog_list_report_text,
36    subnet_catalog_list_report_verbose_text, subnet_catalog_refresh_report_text,
37};
38use thiserror::Error as ThisError;
39#[cfg(test)]
40pub(crate) use time::parse_stale_after_duration;
41pub(crate) use time::{catalog_stale_status, format_utc_timestamp_secs};
42
43pub const CATALOG_SCHEMA_VERSION: u32 = 1;
44pub const MAINNET_NETWORK: &str = "ic";
45pub const MAINNET_REGISTRY_CANISTER_ID: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai";
46pub(crate) const DEFAULT_STALE_AFTER_SECONDS: u64 = 7 * 24 * 60 * 60;
47pub(crate) const DEFAULT_REFRESH_LOCK_STALE_SECONDS: u64 = 30 * 60;
48pub(crate) const DEFAULT_SUBNET_CATALOG_SOURCE_ENDPOINT: &str = DEFAULT_MAINNET_ENDPOINT;
49pub(crate) const SUBNET_CATALOG_LIST_REPORT_SCHEMA_VERSION: u32 = 1;
50pub(crate) const SUBNET_CATALOG_INFO_REPORT_SCHEMA_VERSION: u32 = 1;
51pub(crate) const SUBNET_CATALOG_REFRESH_REPORT_SCHEMA_VERSION: u32 = 1;
52
53///
54/// CatalogError
55///
56#[derive(Debug, ThisError)]
57pub enum CatalogError {
58    #[error(transparent)]
59    Json(#[from] serde_json::Error),
60
61    #[error("unsupported subnet catalog schema version {found}; supported version is {supported}")]
62    UnsupportedSchemaVersion { found: u32, supported: u32 },
63
64    #[error("subnet catalog must contain at least one subnet")]
65    EmptySubnets,
66
67    #[error("subnet catalog must contain at least one routing range")]
68    EmptyRoutingRanges,
69
70    #[error("invalid principal in {field}: {value}: {reason}")]
71    InvalidPrincipal {
72        field: &'static str,
73        value: String,
74        reason: String,
75    },
76
77    #[error("duplicate subnet principal in catalog: {subnet_principal}")]
78    DuplicateSubnet { subnet_principal: String },
79
80    #[error("routing range references unknown subnet: {subnet_principal}")]
81    UnknownRoutingSubnet { subnet_principal: String },
82
83    #[error(
84        "invalid routing range for {subnet_principal}: start {start_canister_id} sorts after end {end_canister_id}"
85    )]
86    InvalidRoutingRange {
87        subnet_principal: String,
88        start_canister_id: String,
89        end_canister_id: String,
90    },
91
92    #[error("subnet principal {subnet_principal} was not found in the cached catalog")]
93    UnknownSubnet { subnet_principal: String },
94
95    #[error("principal prefix {prefix:?} did not match cached subnet principals")]
96    PrincipalPrefixNotFound { prefix: String },
97
98    #[error("principal prefix {prefix:?} is ambiguous; matches: {matches:?}")]
99    AmbiguousPrincipalPrefix {
100        prefix: String,
101        matches: Vec<String>,
102    },
103
104    #[error(
105        "canister principal {canister_principal} was not covered by cached routing ranges at registry_version={registry_version}, catalog_schema_version={catalog_schema_version}"
106    )]
107    RouteNotFound {
108        canister_principal: String,
109        registry_version: u64,
110        catalog_schema_version: u32,
111    },
112}
113
114/// Decode and validate one subnet catalog JSON payload.
115pub fn parse_catalog_json(data: &str) -> Result<SubnetCatalog, CatalogError> {
116    let catalog = serde_json::from_str::<SubnetCatalog>(data)?;
117    catalog.validate()?;
118    Ok(catalog)
119}
120
121/// Render one subnet catalog JSON payload with stable pretty formatting.
122pub fn catalog_to_pretty_json(catalog: &SubnetCatalog) -> Result<String, CatalogError> {
123    Ok(serde_json::to_string_pretty(catalog)?)
124}
125
126/// Parse a textual IC principal into canonical text.
127pub fn canonical_principal_text(value: &str) -> Result<String, CatalogError> {
128    Ok(parse_principal(value, "principal")?.to_text())
129}
130
131pub(crate) fn parse_principal(value: &str, field: &'static str) -> Result<Principal, CatalogError> {
132    Principal::from_text(value).map_err(|err| CatalogError::InvalidPrincipal {
133        field,
134        value: value.to_string(),
135        reason: err.to_string(),
136    })
137}
138
139pub(crate) fn principal_bytes(value: &str, field: &'static str) -> Result<Vec<u8>, CatalogError> {
140    Ok(parse_principal(value, field)?.as_slice().to_vec())
141}
142
143#[cfg(test)]
144mod core_tests;
145#[cfg(test)]
146mod tests;