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 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#[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
45pub 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}