Skip to main content

canic_host/canic_metadata/
mod.rs

1//! Module: canic_metadata
2//!
3//! Responsibility: query and decode the maintained Canic metadata endpoint.
4//! Does not own: metadata production, ICP transport, or list rendering.
5//! Boundary: projects the canonical metadata DTO into its reported Canic version.
6
7#[cfg(test)]
8mod tests;
9
10use crate::{
11    icp::{IcpCli, IcpCommandError, IcpJsonResponseError, decode_json_result_response},
12    protocol_binding::ResolvedProtocolBinding,
13};
14use candid::{CandidType, Deserialize};
15use canic_core::{
16    dto::role::{RoleCapability, RoleOverviewResponse},
17    role_contract::RoleCapabilityKey,
18};
19use thiserror::Error as ThisError;
20
21const ICP_JSON_OUTPUT: &str = "json";
22
23#[derive(CandidType, Deserialize)]
24enum RoleStatusResponse {
25    Overview(RoleOverviewResponse),
26}
27
28///
29/// CanicMetadataQueryError
30///
31
32#[derive(Debug, ThisError)]
33pub enum CanicMetadataQueryError {
34    #[error(transparent)]
35    Icp(#[from] IcpCommandError),
36
37    #[error(transparent)]
38    Response(#[from] IcpJsonResponseError),
39
40    #[error("role Overview conflicts with the exact preselected protocol binding: {0}")]
41    Binding(String),
42}
43
44/// Query the role-owned Overview and return the reported Canic framework version.
45pub fn query_canic_metadata_version(
46    icp: &IcpCli,
47    canister_id: &str,
48    binding: &ResolvedProtocolBinding,
49) -> Result<String, CanicMetadataQueryError> {
50    let output = icp.canister_query_arg_output_with_candid(
51        canister_id,
52        canic_core::protocol::CANIC_PUBLIC_STATUS,
53        "(variant { Overview })",
54        Some(ICP_JSON_OUTPUT),
55        Some(binding.candid_path.as_path()),
56    )?;
57    let overview = parse_canic_metadata_response(&output)?;
58    verify_overview_binding(&overview, binding)?;
59    Ok(overview.metadata.canic_version)
60}
61
62#[cfg(test)]
63fn parse_canic_metadata_version_response(output: &str) -> Result<String, IcpJsonResponseError> {
64    Ok(parse_canic_metadata_response(output)?
65        .metadata
66        .canic_version)
67}
68
69fn parse_canic_metadata_response(
70    output: &str,
71) -> Result<RoleOverviewResponse, IcpJsonResponseError> {
72    let response = decode_json_result_response::<RoleStatusResponse>(output)?;
73    let RoleStatusResponse::Overview(overview) = response;
74    Ok(overview)
75}
76
77fn verify_overview_binding(
78    overview: &RoleOverviewResponse,
79    selected: &ResolvedProtocolBinding,
80) -> Result<(), CanicMetadataQueryError> {
81    let binding = &selected.binding;
82    if overview.role != binding.role {
83        return Err(CanicMetadataQueryError::Binding(
84            "role mismatch".to_string(),
85        ));
86    }
87    if overview.metadata.canic_version != binding.release_identity {
88        return Err(CanicMetadataQueryError::Binding(
89            "release identity mismatch".to_string(),
90        ));
91    }
92    let capabilities = overview
93        .capabilities
94        .iter()
95        .copied()
96        .map(role_capability_name)
97        .collect::<Vec<_>>();
98    let expected = binding
99        .capabilities
100        .iter()
101        .filter(|capability| **capability != RoleCapabilityKey::LocalApplicationAuthorization)
102        .map(|capability| capability.manifest_name())
103        .collect::<Vec<_>>();
104    if capabilities != expected {
105        return Err(CanicMetadataQueryError::Binding(
106            "ordered capability mismatch".to_string(),
107        ));
108    }
109    if overview.protocol_profile_digest != binding.protocol_profile_digest.into_bytes() {
110        return Err(CanicMetadataQueryError::Binding(
111            "profile digest mismatch".to_string(),
112        ));
113    }
114    Ok(())
115}
116
117const fn role_capability_name(capability: RoleCapability) -> &'static str {
118    match capability {
119        RoleCapability::AutomaticTopup => "AutomaticTopup",
120        RoleCapability::ChildProvisioning => "ChildProvisioning",
121        RoleCapability::DelegatedTokenIssuer => "DelegatedTokenIssuer",
122        RoleCapability::DelegatedTokenVerifier => "DelegatedTokenVerifier",
123        RoleCapability::FleetAdmissionProjection => "FleetAdmissionProjection",
124        RoleCapability::FleetCoordinator => "FleetCoordinator",
125        RoleCapability::Icrc21 => "Icrc21",
126        RoleCapability::Index => "Index",
127        RoleCapability::RoleAttestationSigner => "RoleAttestationSigner",
128        RoleCapability::RoleAttestationVerifier => "RoleAttestationVerifier",
129        RoleCapability::Root => "Root",
130        RoleCapability::RootControlPlane => "RootControlPlane",
131        RoleCapability::RootDelegation => "RootDelegation",
132        RoleCapability::Runtime => "Runtime",
133        RoleCapability::Scaling => "Scaling",
134        RoleCapability::Sharding => "Sharding",
135        RoleCapability::WasmStore => "WasmStore",
136    }
137}