canic_host/canic_metadata/
mod.rs1#[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#[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
44pub 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}