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    protocol,
18    role_contract::RoleCapabilityKey,
19};
20use thiserror::Error as ThisError;
21
22const ICP_JSON_OUTPUT: &str = "json";
23
24#[derive(CandidType, Deserialize)]
25enum RoleStatusResponse {
26    Overview(RoleOverviewResponse),
27}
28
29///
30/// CanicMetadataQueryError
31///
32
33#[derive(Debug, ThisError)]
34pub enum CanicMetadataQueryError {
35    #[error(transparent)]
36    Icp(#[from] IcpCommandError),
37
38    #[error(transparent)]
39    Response(#[from] IcpJsonResponseError),
40
41    #[error("role Overview conflicts with the exact preselected protocol binding: {0}")]
42    Binding(String),
43}
44
45/// Query the role-owned Overview and return the reported Canic framework version.
46pub fn query_canic_metadata_version(
47    icp: &IcpCli,
48    canister_id: &str,
49    binding: &ResolvedProtocolBinding,
50) -> Result<String, CanicMetadataQueryError> {
51    let output = icp.canister_query_arg_output_with_candid(
52        canister_id,
53        protocol::CANIC_STATUS,
54        "(variant { Overview })",
55        Some(ICP_JSON_OUTPUT),
56        Some(binding.candid_path.as_path()),
57    )?;
58    let overview = parse_canic_metadata_response(&output)?;
59    verify_overview_binding(&overview, binding)?;
60    Ok(overview.metadata.canic_version)
61}
62
63#[cfg(test)]
64fn parse_canic_metadata_version_response(output: &str) -> Result<String, IcpJsonResponseError> {
65    Ok(parse_canic_metadata_response(output)?
66        .metadata
67        .canic_version)
68}
69
70fn parse_canic_metadata_response(
71    output: &str,
72) -> Result<RoleOverviewResponse, IcpJsonResponseError> {
73    let response = decode_json_result_response::<RoleStatusResponse>(output)?;
74    let RoleStatusResponse::Overview(overview) = response;
75    Ok(overview)
76}
77
78fn verify_overview_binding(
79    overview: &RoleOverviewResponse,
80    selected: &ResolvedProtocolBinding,
81) -> Result<(), CanicMetadataQueryError> {
82    let binding = &selected.binding;
83    if overview.role != binding.role {
84        return Err(CanicMetadataQueryError::Binding(
85            "role mismatch".to_string(),
86        ));
87    }
88    if overview.metadata.canic_version != binding.release_identity {
89        return Err(CanicMetadataQueryError::Binding(
90            "release identity mismatch".to_string(),
91        ));
92    }
93    let capabilities = overview
94        .capabilities
95        .iter()
96        .copied()
97        .map(role_capability_name)
98        .collect::<Vec<_>>();
99    let expected = binding
100        .capabilities
101        .iter()
102        .filter(|capability| **capability != RoleCapabilityKey::LocalApplicationAuthorization)
103        .map(|capability| capability.manifest_name())
104        .collect::<Vec<_>>();
105    if capabilities != expected {
106        return Err(CanicMetadataQueryError::Binding(
107            "ordered capability mismatch".to_string(),
108        ));
109    }
110    if overview.protocol_profile_digest != binding.protocol_profile_digest.into_bytes() {
111        return Err(CanicMetadataQueryError::Binding(
112            "profile digest mismatch".to_string(),
113        ));
114    }
115    Ok(())
116}
117
118const fn role_capability_name(capability: RoleCapability) -> &'static str {
119    match capability {
120        RoleCapability::AutomaticTopup => "AutomaticTopup",
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::Runtime => "Runtime",
132        RoleCapability::Scaling => "Scaling",
133        RoleCapability::Sharding => "Sharding",
134        RoleCapability::WasmStore => "WasmStore",
135    }
136}