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, topology projection, or ICP CLI command execution.
5//! Boundary: decodes canonical Candid responses and preserves typed transport and endpoint errors.
6
7mod cbor;
8mod status;
9#[cfg(test)]
10mod tests;
11mod transport;
12
13use self::transport::local_query;
14use std::path::Path;
15
16use candid::{CandidType, Decode, Deserialize, Encode};
17use canic_core::{
18    dto::{
19        error::Error as CanicError,
20        role::CycleBalanceStatusResponse,
21        runtime::{CanicReadinessStatus, ReadinessStatus},
22    },
23    ids::BuildNetwork,
24    protocol::CANIC_STATUS,
25};
26use thiserror::Error as ThisError;
27
28use crate::icp_config::{
29    IcpConfigError, resolve_current_canic_icp_root, resolve_icp_build_network_from_root,
30};
31
32pub use self::status::local_replica_status_reachable_from_root;
33pub(crate) use self::{
34    status::local_replica_root_key_from_root, transport::local_replica_endpoint_from_root,
35};
36
37fn nonempty_text(text: &str) -> Option<String> {
38    let trimmed = text.trim();
39    (!trimmed.is_empty()).then(|| trimmed.to_string())
40}
41
42fn decode_cycle_balance_response(bytes: &[u8]) -> Result<u128, ReplicaQueryError> {
43    let result = Decode!(bytes, Result<RoleStatusResponse, CanicError>)
44        .map_err(ReplicaQueryError::Candid)?;
45    match result.map_err(ReplicaQueryError::Canister)? {
46        RoleStatusResponse::CycleBalance(response) => Ok(response.cycles),
47        RoleStatusResponse::Readiness(_) => Err(unexpected_role_status_response()),
48    }
49}
50
51#[derive(CandidType)]
52enum RoleStatusRequest {
53    CycleBalance,
54    Readiness,
55}
56
57#[derive(CandidType, Deserialize)]
58enum RoleStatusResponse {
59    CycleBalance(CycleBalanceStatusResponse),
60    Readiness(CanicReadinessStatus),
61}
62
63///
64/// ReplicaQueryError
65///
66
67#[derive(Debug, ThisError)]
68pub enum ReplicaQueryError {
69    #[error(transparent)]
70    Candid(candid::Error),
71
72    #[error("{0}")]
73    Canister(CanicError),
74
75    #[error("{0}")]
76    Cbor(String),
77
78    #[error(transparent)]
79    Io(#[from] std::io::Error),
80
81    #[error("{0}")]
82    Query(String),
83
84    #[error("local replica rejected query: code={code} message={message}")]
85    Rejected { code: u64, message: String },
86}
87
88impl From<cbor::CborError> for ReplicaQueryError {
89    // Convert CBOR encode/decode failures.
90    fn from(err: cbor::CborError) -> Self {
91        Self::Cbor(err.to_string())
92    }
93}
94
95/// Resolve whether the selected environment uses the direct replica transport.
96pub fn uses_local_replica_transport(
97    environment: Option<&str>,
98    icp_root: Option<&Path>,
99) -> Result<bool, IcpConfigError> {
100    let Some(environment) = environment else {
101        return Ok(true);
102    };
103    if environment.starts_with("http://") {
104        return Ok(true);
105    }
106
107    let discovered_root;
108    let root = if let Some(root) = icp_root {
109        root
110    } else {
111        discovered_root = resolve_current_canic_icp_root()?;
112        &discovered_root
113    };
114    Ok(resolve_icp_build_network_from_root(root, environment)? == BuildNetwork::Local)
115}
116
117/// Query role-owned readiness directly through the local replica HTTP API.
118pub(crate) fn query_ready(
119    environment: Option<&str>,
120    canister: &str,
121    icp_root: Option<&Path>,
122) -> Result<bool, ReplicaQueryError> {
123    let arg = Encode!(&RoleStatusRequest::Readiness).map_err(ReplicaQueryError::Candid)?;
124    let bytes = local_query(environment, canister, CANIC_STATUS, &arg, icp_root)?;
125    let result = Decode!(&bytes, Result<RoleStatusResponse, CanicError>)
126        .map_err(ReplicaQueryError::Candid)?;
127    match result.map_err(ReplicaQueryError::Canister)? {
128        RoleStatusResponse::Readiness(response) => Ok(response.status == ReadinessStatus::Ready),
129        RoleStatusResponse::CycleBalance(_) => Err(unexpected_role_status_response()),
130    }
131}
132
133/// Query role-owned cycle balance directly through the local replica HTTP API.
134pub(crate) fn query_cycle_balance(
135    environment: Option<&str>,
136    canister: &str,
137    icp_root: Option<&Path>,
138) -> Result<u128, ReplicaQueryError> {
139    let arg = Encode!(&RoleStatusRequest::CycleBalance).map_err(ReplicaQueryError::Candid)?;
140    let bytes = local_query(environment, canister, CANIC_STATUS, &arg, icp_root)?;
141    decode_cycle_balance_response(&bytes)
142}
143
144fn unexpected_role_status_response() -> ReplicaQueryError {
145    ReplicaQueryError::Query("role returned a differently correlated status response".to_string())
146}