Skip to main content

canic_subnet_catalog/
lib.rs

1//! Pure cached IC subnet catalog model and resolver for Canic host tools.
2
3pub mod model;
4pub mod resolver;
5
6use candid::Principal;
7pub use model::{
8    ClassificationSource, GeographicScope, RoutingRange, SubnetCatalog, SubnetInfo, SubnetKind,
9    SubnetSpecialization,
10};
11pub use resolver::{ResolveAs, ResolvedSubnet, ResolvedSubnetSubject};
12use thiserror::Error as ThisError;
13
14pub const CATALOG_SCHEMA_VERSION: u32 = 1;
15pub const MAINNET_NETWORK: &str = "ic";
16pub const MAINNET_REGISTRY_CANISTER_ID: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai";
17
18///
19/// CatalogError
20///
21#[derive(Debug, ThisError)]
22pub enum CatalogError {
23    #[error(transparent)]
24    Json(#[from] serde_json::Error),
25
26    #[error("unsupported subnet catalog schema version {found}; supported version is {supported}")]
27    UnsupportedSchemaVersion { found: u32, supported: u32 },
28
29    #[error("subnet catalog must contain at least one subnet")]
30    EmptySubnets,
31
32    #[error("subnet catalog must contain at least one routing range")]
33    EmptyRoutingRanges,
34
35    #[error("invalid principal in {field}: {value}: {reason}")]
36    InvalidPrincipal {
37        field: &'static str,
38        value: String,
39        reason: String,
40    },
41
42    #[error("duplicate subnet principal in catalog: {subnet_principal}")]
43    DuplicateSubnet { subnet_principal: String },
44
45    #[error("routing range references unknown subnet: {subnet_principal}")]
46    UnknownRoutingSubnet { subnet_principal: String },
47
48    #[error(
49        "invalid routing range for {subnet_principal}: start {start_canister_id} sorts after end {end_canister_id}"
50    )]
51    InvalidRoutingRange {
52        subnet_principal: String,
53        start_canister_id: String,
54        end_canister_id: String,
55    },
56
57    #[error("subnet principal {subnet_principal} was not found in the cached catalog")]
58    UnknownSubnet { subnet_principal: String },
59
60    #[error("principal prefix {prefix:?} did not match cached subnet principals")]
61    PrincipalPrefixNotFound { prefix: String },
62
63    #[error("principal prefix {prefix:?} is ambiguous; matches: {matches:?}")]
64    AmbiguousPrincipalPrefix {
65        prefix: String,
66        matches: Vec<String>,
67    },
68
69    #[error(
70        "canister principal {canister_principal} was not covered by cached routing ranges at registry_version={registry_version}, catalog_schema_version={catalog_schema_version}"
71    )]
72    RouteNotFound {
73        canister_principal: String,
74        registry_version: u64,
75        catalog_schema_version: u32,
76    },
77}
78
79/// Decode and validate one subnet catalog JSON payload.
80pub fn parse_catalog_json(data: &str) -> Result<SubnetCatalog, CatalogError> {
81    let catalog = serde_json::from_str::<SubnetCatalog>(data)?;
82    catalog.validate()?;
83    Ok(catalog)
84}
85
86/// Render one subnet catalog JSON payload with stable pretty formatting.
87pub fn catalog_to_pretty_json(catalog: &SubnetCatalog) -> Result<String, CatalogError> {
88    Ok(serde_json::to_string_pretty(catalog)?)
89}
90
91/// Parse a textual IC principal into canonical text.
92pub fn canonical_principal_text(value: &str) -> Result<String, CatalogError> {
93    Ok(parse_principal(value, "principal")?.to_text())
94}
95
96pub(crate) fn parse_principal(value: &str, field: &'static str) -> Result<Principal, CatalogError> {
97    Principal::from_text(value).map_err(|err| CatalogError::InvalidPrincipal {
98        field,
99        value: value.to_string(),
100        reason: err.to_string(),
101    })
102}
103
104pub(crate) fn principal_bytes(value: &str, field: &'static str) -> Result<Vec<u8>, CatalogError> {
105    Ok(parse_principal(value, field)?.as_slice().to_vec())
106}
107
108#[cfg(test)]
109mod tests;