Skip to main content

canic_host/replica_query/
mod.rs

1//! Module: replica_query
2//!
3//! Responsibility: query maintained Canic endpoints through a direct local replica transport.
4//! Does not own: endpoint DTOs, registry projection, or ICP CLI command execution.
5//! Boundary: decodes canonical Candid responses and preserves typed transport and endpoint errors.
6
7mod cbor;
8mod status;
9mod transport;
10mod wire;
11
12use self::{
13    transport::{local_query, local_query_from_root},
14    wire::{
15        decode_bootstrap_status_response, decode_cycle_balance_response,
16        decode_subnet_registry_response,
17    },
18};
19use crate::registry::{RegistryEntry, RegistryParseError, registry_entries_from_response};
20use std::{error::Error, fmt, path::Path};
21
22use candid::Decode;
23use canic_core::dto::{
24    error::Error as CanicError, state::BootstrapStatusResponse, topology::SubnetRegistryResponse,
25};
26
27pub use self::{
28    status::{local_replica_root_key_from_root, local_replica_status_reachable_from_root},
29    transport::local_replica_endpoint_from_root,
30};
31
32///
33/// ReplicaQueryError
34///
35
36#[derive(Debug)]
37pub enum ReplicaQueryError {
38    Candid(candid::Error),
39    Canister(CanicError),
40    Cbor(String),
41    Io(std::io::Error),
42    Query(String),
43    Registry(RegistryParseError),
44    Rejected { code: u64, message: String },
45}
46
47impl fmt::Display for ReplicaQueryError {
48    // Render local replica query failures as compact operator diagnostics.
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Candid(err) => write!(formatter, "{err}"),
52            Self::Canister(err) => write!(formatter, "{err}"),
53            Self::Cbor(err) => formatter.write_str(err),
54            Self::Io(err) => write!(formatter, "{err}"),
55            Self::Query(message) => write!(formatter, "{message}"),
56            Self::Registry(err) => write!(formatter, "{err}"),
57            Self::Rejected { code, message } => {
58                write!(
59                    formatter,
60                    "local replica rejected query: code={code} message={message}"
61                )
62            }
63        }
64    }
65}
66
67impl Error for ReplicaQueryError {
68    // Preserve structured source errors for I/O and serialization failures.
69    fn source(&self) -> Option<&(dyn Error + 'static)> {
70        match self {
71            Self::Candid(err) => Some(err),
72            Self::Io(err) => Some(err),
73            Self::Registry(err) => Some(err),
74            Self::Canister(_) | Self::Cbor(_) | Self::Query(_) | Self::Rejected { .. } => None,
75        }
76    }
77}
78
79impl From<std::io::Error> for ReplicaQueryError {
80    // Convert local socket and process I/O failures.
81    fn from(err: std::io::Error) -> Self {
82        Self::Io(err)
83    }
84}
85
86impl From<cbor::CborError> for ReplicaQueryError {
87    // Convert CBOR encode/decode failures.
88    fn from(err: cbor::CborError) -> Self {
89        Self::Cbor(err.to_string())
90    }
91}
92
93impl From<RegistryParseError> for ReplicaQueryError {
94    fn from(err: RegistryParseError) -> Self {
95        Self::Registry(err)
96    }
97}
98
99/// Return whether the selected environment should use direct local replica queries.
100#[must_use]
101pub fn should_use_local_replica_query(environment: Option<&str>) -> bool {
102    environment
103        .is_none_or(|environment| environment == "local" || environment.starts_with("http://"))
104}
105
106/// Query `canic_ready` directly through the local replica HTTP API.
107pub fn query_ready(environment: Option<&str>, canister: &str) -> Result<bool, ReplicaQueryError> {
108    let bytes = local_query(environment, canister, "canic_ready")?;
109    Decode!(&bytes, bool).map_err(ReplicaQueryError::Candid)
110}
111
112/// Query `canic_ready` using the configured port from one ICP root.
113pub fn query_ready_from_root(
114    environment: Option<&str>,
115    canister: &str,
116    icp_root: &Path,
117) -> Result<bool, ReplicaQueryError> {
118    let bytes = local_query_from_root(environment, canister, "canic_ready", icp_root)?;
119    Decode!(&bytes, bool).map_err(ReplicaQueryError::Candid)
120}
121
122/// Query `canic_bootstrap_status` using the configured port from one ICP root.
123pub fn query_bootstrap_status_from_root(
124    environment: Option<&str>,
125    canister: &str,
126    icp_root: &Path,
127) -> Result<BootstrapStatusResponse, ReplicaQueryError> {
128    let bytes = local_query_from_root(environment, canister, "canic_bootstrap_status", icp_root)?;
129    decode_bootstrap_status_response(&bytes)
130}
131
132/// Query `canic_cycle_balance` directly through the local replica HTTP API.
133pub(crate) fn query_cycle_balance(
134    environment: Option<&str>,
135    canister: &str,
136) -> Result<u128, ReplicaQueryError> {
137    let bytes = local_query(environment, canister, "canic_cycle_balance")?;
138    decode_cycle_balance_response(&bytes)
139}
140
141/// Query `canic_cycle_balance` using the configured port from one ICP root.
142pub fn query_cycle_balance_from_root(
143    environment: Option<&str>,
144    canister: &str,
145    icp_root: &Path,
146) -> Result<u128, ReplicaQueryError> {
147    let bytes = local_query_from_root(environment, canister, "canic_cycle_balance", icp_root)?;
148    decode_cycle_balance_response(&bytes)
149}
150
151/// Query `canic_subnet_registry` and return validated host entries.
152pub fn query_subnet_registry_entries(
153    environment: Option<&str>,
154    root: &str,
155) -> Result<Vec<RegistryEntry>, ReplicaQueryError> {
156    let response = query_subnet_registry_response(environment, root)?;
157    registry_entries_from_response(response).map_err(Into::into)
158}
159
160/// Query `canic_subnet_registry` from one ICP root and return validated host entries.
161pub fn query_subnet_registry_entries_from_root(
162    environment: Option<&str>,
163    root: &str,
164    icp_root: &Path,
165) -> Result<Vec<RegistryEntry>, ReplicaQueryError> {
166    let response = query_subnet_registry_response_from_root(environment, root, icp_root)?;
167    registry_entries_from_response(response).map_err(Into::into)
168}
169
170/// Query `canic_subnet_registry` using the configured port from one ICP root and return roles.
171pub fn query_subnet_registry_roles_from_root(
172    environment: Option<&str>,
173    root: &str,
174    icp_root: &Path,
175) -> Result<Vec<String>, ReplicaQueryError> {
176    Ok(
177        query_subnet_registry_entries_from_root(environment, root, icp_root)?
178            .into_iter()
179            .filter_map(|entry| entry.role)
180            .collect(),
181    )
182}
183
184fn query_subnet_registry_response(
185    environment: Option<&str>,
186    root: &str,
187) -> Result<SubnetRegistryResponse, ReplicaQueryError> {
188    let bytes = local_query(environment, root, "canic_subnet_registry")?;
189    decode_subnet_registry_response(&bytes)
190}
191
192fn query_subnet_registry_response_from_root(
193    environment: Option<&str>,
194    root: &str,
195    icp_root: &Path,
196) -> Result<SubnetRegistryResponse, ReplicaQueryError> {
197    let bytes = local_query_from_root(environment, root, "canic_subnet_registry", icp_root)?;
198    decode_subnet_registry_response(&bytes)
199}