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