Skip to main content

canic_host/replica_query/
mod.rs

1pub use self::{
2    status::{local_replica_root_key_from_root, local_replica_status_reachable_from_root},
3    transport::local_replica_endpoint_from_root,
4};
5use self::{
6    transport::{local_query, local_query_from_root},
7    wire::{
8        SubnetRegistryResponseWire, decode_bootstrap_status_response,
9        decode_cycle_balance_response, decode_subnet_registry_response,
10    },
11};
12use candid::Decode;
13use canic_core::dto::state::BootstrapStatusResponse;
14use std::{error::Error, fmt, path::Path};
15
16mod status;
17mod transport;
18mod wire;
19
20///
21/// ReplicaQueryError
22///
23
24#[derive(Debug)]
25pub enum ReplicaQueryError {
26    Io(std::io::Error),
27    Cbor(serde_cbor::Error),
28    Json(serde_json::Error),
29    Query(String),
30    Rejected { code: u64, message: String },
31}
32
33impl fmt::Display for ReplicaQueryError {
34    // Render local replica query failures as compact operator diagnostics.
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::Io(err) => write!(formatter, "{err}"),
38            Self::Cbor(err) => write!(formatter, "{err}"),
39            Self::Json(err) => write!(formatter, "{err}"),
40            Self::Query(message) => write!(formatter, "{message}"),
41            Self::Rejected { code, message } => {
42                write!(
43                    formatter,
44                    "local replica rejected query: code={code} message={message}"
45                )
46            }
47        }
48    }
49}
50
51impl Error for ReplicaQueryError {
52    // Preserve structured source errors for I/O and serialization failures.
53    fn source(&self) -> Option<&(dyn Error + 'static)> {
54        match self {
55            Self::Io(err) => Some(err),
56            Self::Cbor(err) => Some(err),
57            Self::Json(err) => Some(err),
58            Self::Query(_) | Self::Rejected { .. } => None,
59        }
60    }
61}
62
63impl From<std::io::Error> for ReplicaQueryError {
64    // Convert local socket and process I/O failures.
65    fn from(err: std::io::Error) -> Self {
66        Self::Io(err)
67    }
68}
69
70impl From<serde_cbor::Error> for ReplicaQueryError {
71    // Convert CBOR encode/decode failures.
72    fn from(err: serde_cbor::Error) -> Self {
73        Self::Cbor(err)
74    }
75}
76
77impl From<serde_json::Error> for ReplicaQueryError {
78    // Convert JSON rendering failures.
79    fn from(err: serde_json::Error) -> Self {
80        Self::Json(err)
81    }
82}
83
84/// Return whether the selected network should use direct local replica queries.
85#[must_use]
86pub fn should_use_local_replica_query(network: Option<&str>) -> bool {
87    network.is_none_or(|network| network == "local" || network.starts_with("http://"))
88}
89
90/// Query `canic_ready` directly through the local replica HTTP API.
91pub fn query_ready(network: Option<&str>, canister: &str) -> Result<bool, ReplicaQueryError> {
92    let bytes = local_query(network, canister, "canic_ready")?;
93    Decode!(&bytes, bool).map_err(|err| ReplicaQueryError::Query(err.to_string()))
94}
95
96/// Query `canic_ready` using the configured port from one ICP root.
97pub fn query_ready_from_root(
98    network: Option<&str>,
99    canister: &str,
100    icp_root: &Path,
101) -> Result<bool, ReplicaQueryError> {
102    let bytes = local_query_from_root(network, canister, "canic_ready", icp_root)?;
103    Decode!(&bytes, bool).map_err(|err| ReplicaQueryError::Query(err.to_string()))
104}
105
106/// Query `canic_bootstrap_status` using the configured port from one ICP root.
107pub fn query_bootstrap_status_from_root(
108    network: Option<&str>,
109    canister: &str,
110    icp_root: &Path,
111) -> Result<BootstrapStatusResponse, ReplicaQueryError> {
112    let bytes = local_query_from_root(network, canister, "canic_bootstrap_status", icp_root)?;
113    decode_bootstrap_status_response(&bytes)
114}
115
116/// Query `canic_cycle_balance` using the configured port from one ICP root.
117pub fn query_cycle_balance_from_root(
118    network: Option<&str>,
119    canister: &str,
120    icp_root: &Path,
121) -> Result<u128, ReplicaQueryError> {
122    let bytes = local_query_from_root(network, canister, "canic_cycle_balance", icp_root)?;
123    decode_cycle_balance_response(&bytes)
124}
125
126/// Parse common JSON shapes returned by command-line calls for `canic_ready`.
127#[must_use]
128pub fn parse_ready_json_value(data: &serde_json::Value) -> bool {
129    match data {
130        serde_json::Value::Bool(value) => *value,
131        serde_json::Value::String(value) => value.trim() == "(true)",
132        serde_json::Value::Array(values) => values.iter().any(parse_ready_json_value),
133        serde_json::Value::Object(map) => map.values().any(parse_ready_json_value),
134        _ => false,
135    }
136}
137
138/// Query `canic_subnet_registry` and render JSON in the CLI response shape.
139pub fn query_subnet_registry_json(
140    network: Option<&str>,
141    root: &str,
142) -> Result<String, ReplicaQueryError> {
143    let response = query_subnet_registry_response(network, root)?;
144    serde_json::to_string(&response.to_cli_json()).map_err(ReplicaQueryError::from)
145}
146
147/// Query `canic_subnet_registry` using the configured port from one ICP root.
148pub fn query_subnet_registry_json_from_root(
149    network: Option<&str>,
150    root: &str,
151    icp_root: &Path,
152) -> Result<String, ReplicaQueryError> {
153    let response = query_subnet_registry_response_from_root(network, root, icp_root)?;
154    serde_json::to_string(&response.to_cli_json()).map_err(ReplicaQueryError::from)
155}
156
157/// Query `canic_subnet_registry` using the configured port from one ICP root and return roles.
158pub fn query_subnet_registry_roles_from_root(
159    network: Option<&str>,
160    root: &str,
161    icp_root: &Path,
162) -> Result<Vec<String>, ReplicaQueryError> {
163    Ok(query_subnet_registry_response_from_root(network, root, icp_root)?.roles())
164}
165
166fn query_subnet_registry_response(
167    network: Option<&str>,
168    root: &str,
169) -> Result<SubnetRegistryResponseWire, ReplicaQueryError> {
170    let bytes = local_query(network, root, "canic_subnet_registry")?;
171    decode_subnet_registry_response(&bytes)
172}
173
174fn query_subnet_registry_response_from_root(
175    network: Option<&str>,
176    root: &str,
177    icp_root: &Path,
178) -> Result<SubnetRegistryResponseWire, ReplicaQueryError> {
179    let bytes = local_query_from_root(network, root, "canic_subnet_registry", icp_root)?;
180    decode_subnet_registry_response(&bytes)
181}
182
183#[cfg(test)]
184mod tests;