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::Decode;
17use canic_core::{dto::error::Error as CanicError, ids::BuildNetwork};
18use thiserror::Error as ThisError;
19
20use crate::icp_config::{
21    IcpConfigError, resolve_current_canic_icp_root, resolve_icp_build_network_from_root,
22};
23
24pub use self::status::local_replica_status_reachable_from_root;
25pub(crate) use self::{
26    status::local_replica_root_key_from_root, transport::local_replica_endpoint_from_root,
27};
28
29fn nonempty_text(text: &str) -> Option<String> {
30    let trimmed = text.trim();
31    (!trimmed.is_empty()).then(|| trimmed.to_string())
32}
33
34fn decode_cycle_balance_response(bytes: &[u8]) -> Result<u128, ReplicaQueryError> {
35    let result = Decode!(bytes, Result<u128, CanicError>).map_err(ReplicaQueryError::Candid)?;
36    result.map_err(ReplicaQueryError::Canister)
37}
38
39///
40/// ReplicaQueryError
41///
42
43#[derive(Debug, ThisError)]
44pub enum ReplicaQueryError {
45    #[error(transparent)]
46    Candid(candid::Error),
47
48    #[error("{0}")]
49    Canister(CanicError),
50
51    #[error("{0}")]
52    Cbor(String),
53
54    #[error(transparent)]
55    Io(#[from] std::io::Error),
56
57    #[error("{0}")]
58    Query(String),
59
60    #[error("local replica rejected query: code={code} message={message}")]
61    Rejected { code: u64, message: String },
62}
63
64impl From<cbor::CborError> for ReplicaQueryError {
65    // Convert CBOR encode/decode failures.
66    fn from(err: cbor::CborError) -> Self {
67        Self::Cbor(err.to_string())
68    }
69}
70
71/// Resolve whether the selected environment uses the direct replica transport.
72pub fn uses_local_replica_transport(
73    environment: Option<&str>,
74    icp_root: Option<&Path>,
75) -> Result<bool, IcpConfigError> {
76    let Some(environment) = environment else {
77        return Ok(true);
78    };
79    if environment.starts_with("http://") {
80        return Ok(true);
81    }
82
83    let discovered_root;
84    let root = if let Some(root) = icp_root {
85        root
86    } else {
87        discovered_root = resolve_current_canic_icp_root()?;
88        &discovered_root
89    };
90    Ok(resolve_icp_build_network_from_root(root, environment)? == BuildNetwork::Local)
91}
92
93/// Query `canic_ready` directly through the local replica HTTP API.
94pub(crate) fn query_ready(
95    environment: Option<&str>,
96    canister: &str,
97    icp_root: Option<&Path>,
98) -> Result<bool, ReplicaQueryError> {
99    let bytes = local_query(environment, canister, "canic_ready", icp_root)?;
100    Decode!(&bytes, bool).map_err(ReplicaQueryError::Candid)
101}
102
103/// Query `canic_cycle_balance` directly through the local replica HTTP API.
104pub(crate) fn query_cycle_balance(
105    environment: Option<&str>,
106    canister: &str,
107    icp_root: Option<&Path>,
108) -> Result<u128, ReplicaQueryError> {
109    let bytes = local_query(environment, canister, "canic_cycle_balance", icp_root)?;
110    decode_cycle_balance_response(&bytes)
111}